중복 제거
전체적인 설계가 없는 상태에서 코드를 작성하다보면 중복된 코드가 나타나는 경우가 많다. 이렇게 중복된 코드는 대부분 하나가 수정되면 다른 곳도 수정이 되어야 한다. 따라서 중복된 코드를 한 곳에서 관리해주지 않으면 관리 포인트가 늘어나게 된다.
public class UserRepository {
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/user", "username", "pwd");
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/user", "username", "pwd");
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
위 코드에서 커넥션을 생성하는 코드가 두 메소드에서 중복되었다. 만약 DB를 Oracle로 바꾸게 된다면 두 곳에서 코드를 수정해주어야 한다. 메소드가 2개라면 큰 문제가 없지만, 실제로는 수백개가 있을 수도 있다.
중복을 제거하는 방법으로는 메소드로 추출하는 방법과 클래스로 분리하는 방법이 있을 것이다. 클래스 내부에서만 사용한다면 private 메소드로 분리하는 것이 편하다. 반면, 다른 클래스에서도 사용해야 한다면 클래스를 분리하는 것이 낫다.
public class UserRepository {
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
private static Connection getConnection() {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/user", "username", "pwd");
}
}
메소드로 중복 코드를 추출하고 나니, DB를 변경할 때 getConnection 메소드만 바꾸면 되도록 바뀌었다.
그런데 관리 포인트는 하나로 합쳐졌지만, 커넥션을 만드는 코드와 데이터를 조회하는 코드는 여전히 같은 클래스에 있다. DB의 종류가 바뀌면 커넥션을 만드는 코드만 수정하면 되고, 조회하는 코드는 변하지 않는다. 반대로 DB는 그대로이지만 다른 테이블에서 데이터를 조회한다면 조회 코드만 바뀌어야 한다. 그러나 같은 클래스에 존재할 경우, 클래스는 두 경우 모두 수정되어야 한다.
책임의 분리
객체지향에서는 클래스를 하나의 '책임'만 가지도록 분리하기를 권장한다. '책임'이라는 단어가 모호하지만, 이 책에서는 '수정해야 할 이유'라고 정의한다. 위의 예시에서는 '커넥션을 만드는 책임'과 'DB를 조회하는 책임'으로 명확하게 분리할 수 있다. 따라서 커넥션을 만드는 코드를 별도의 클래스로 분리했다고 하자.
public class UserRepository {
private MySQLConnectionMaker cm = new MySQLConnectionMaker();
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
이제 MySQLConnectionMaker 클래스는 DB 조회 코드가 변해도 절대 변하지 않을 것이다. 그러나 getUser에서 MySQL이 아니라 Oracle을 사용하고 싶다면? 여전히 해당 클래스의 코드를 수정해줘야 한다.
public class UserRepository {
private OracleConnectionMaker cm = new OracleConnectionMaker();
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
객체지향의 단일 책임 원칙에 따라 클래스를 분리했지만, 여전히 '수정해야 할 이유'는 완벽하게 분리되지 못했다.
역할과 구현의 분리
UserRepository는 현재 '수정해야 할 이유'가 두 가지다. 하나는 DB 조회 로직의 변경이고, 하나는 DB 종류의 변경이다. 따라서 우리는 DB 종류 변경에 따라 변하지 않도록 만들어야 한다.
MySQLConnectionMaker와 OracleConnectionMaker는 둘 다 커넥션을 만들어주는 역할을 수행한다. 따라서 이러한 역할을 쉽게 교체할 수 있도록 역할과 구현을 분리해서 생각할 수 있다. ConnectionMaker라는 인터페이스를 만들고, MySQLConnectionMaker와 OracleConnectionMaker가 인터페이스를 구현하도록 변경한다. 그리고 ConnectionMaker를 사용하도록 코드를 수정한다.
public class UserRepository {
private ConnectionMaker cm = new ConnectionMaker();
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
위의 코드는 컴파일 에러가 발생한다. 인터페이스는 단독으로 인스턴스를 생성할 수 없기 때문이다.
public class UserRepository {
private ConnectionMaker cm = new MySQLConnectionMaker();
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
다시 코드에서 MySQLConnectionMaker를 생성하면 총체적 난국이다. DB 변경시에 코드를 수정해야하는 것은 물론이고, 쓸 데 없이 인터페이스만 만든 꼴이다. 구체 클래스에 의존하지 않으려면 외부에서 주입을 받아야 한다.
public class UserRepository {
private ConnectionMaker cm;
public UserRepository(ConnectionMaker cm) {
this.cm = cm;
}
public List<User> getAllUsers() {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user");
/* ~~ 생략 ~~ */
}
public User getUser(Long id) {
/* ~~ 생략 ~~ */
Connection c = cm.getConnection();
PreparedStatement ps = c.prepareStatement("select * from user where user.id = ?");
/* ~~ 생략 ~~ */
}
}
이제 MySQL과 Oracle에 대한 의존성을 완전히 없앴다. 대신 MySQLConnectionMaker와 OracleConnectionMaker의 생성과 주입을 외부로 떠넘겼다. Spring 책에서 여태까지 다른 이야기를 한 것은 바로 이것 때문이다. 바로 Spring이 이 문제를 해결해주기 때문이다.
3줄 요약
- 중복된 코드를 제거하고, 클래스를 분리하자
- 클래스 분리의 기준은 '수정해야 할 이유'가 하나가 되도록 하는 것
- 구체 클래스를 의존하지 말고 인터페이스에 의존하자
'Spring Framework > 토비의 스프링' 카테고리의 다른 글
[Spring] 토비의 스프링 3.1(4) - PSA(서비스 추상화) (0) | 2023.03.11 |
---|---|
[Spring] 토비의 스프링 3.1(3) - AOP (1) | 2023.03.10 |
[Spring] 토비의 스프링 3.1(2) - IoC/DI (0) | 2023.03.08 |