본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

생성 사용 관심사 분리와 의존성 관리 기법

by 민휘 2023. 3. 2.
관심사를 별도의 단위로 분리해야하는 이유는 관심사 분리로 변경의 파급효과를 제한하여 코드 수정을 최소화할 수 있기 때문이다.

 

이 예제에서는 메소드 추출로 코드 중복을 제거하고, 상속과 인터페이스를 이용해 생성 책임을 분리해보고 차이점을 비교해본다. 테스트 메소드를 작성해 코드가 수정될 때마다 기능이 잘 동작하는지 확인하는 리팩토링도 해본다. 마지막에는 의존성과 관련된 객체지향 개념인 OCP, 응집도와 결합도에 대해 이야기한다.

 

아래 코드의 add와 get은 UserDao의 메소드로, DB로부터 커넥션을 받아와 sql문을 준비하고 실행시켜 값을 사용한다. 사용한 리소스를 닫고 마무리한다. 보자마자 코드 스멜이 난다. 우선 add와 get에 유사한 코드가 중복된다. 그리고 하나의 메소드가 너무 많은 일을 하고 있다.

public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/toby", "root", "star0826");

        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/toby", "root", "star0826");

        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();

        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }

 

메소드 추출로 중복 없애기

우선 코드 중복부터 없애보자. 커넥션 풀을 받아오는 부분은 url이나 로그인 정보가 변경되면 드라이버로부터 커넥션을 받아오는 모든 부분을 함께 변경해야한다. 이런 경우 메소드 추출로 코드 중복을 없앨 수 있다. 코드 변경을 마친 후 테스트 코드를 다시 실행해본다. 기능에 영향을 주지 않으면서 내부 구조는 변경하는 리팩토링을 해냈다.

public void add(User user) throws ClassNotFoundException, SQLException {
	Connection c = getConnection();
	// ...
}

public User get(String id) throws ClassNotFoundException, SQLException {
	Connection c = getConnection();
	// ...
}

private Connection getConnection() throws ClassNotFoundException, SQLException {
	Class.forName("com.mysql.cj.jdbc.Driver");
	return DriverManager.getConnection("jdbc:mysql://localhost:3306/toby", "root", "star0826");
}

 

생성과 사용 책임 분리하기

이와중에 새로운 요구사항이 들어왔다. 클라이언트에게 이 UserDao 모듈을 팔건데, 커넥션을 받아오는 부분은 클라이언트가 구현하도록 해야 한다. 마침 관심사 분리를 할 예정이었던 우리에게 반가운 소식이다. 커넥션을 생성하는 부분을 사용하는 부분으로부터 분리한다. 생성 로직은 클라이언트가 구현하도록 하고, 사용 로직은 추상 클래스에 남겨두자. 사용하는 로직은 생성된 커넥션을 받아와 사용한다.

public abstract class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = getConnection();
	  // sql 준비하고 실행
		// 리소스 정리
	}
	
	public User get(String id) throws ClassNotFoundException, SQLException {
		Connection c = getConnection();
		// sql 준비하고 실행
		// 리소스 정리
	}
	protected abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

public class NUserDao extends UserDao {
    @Override
    protected Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/toby", "root", "star0826");
    }
}

public class DUserDao extends UserDao {
    @Override
    protected Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        return DriverManager.getConnection("jdbc:mysql://daum:3306/toby", "daum", "daum1234");
    }
}

사용하는 책임을 가진 UserDao 추상 클래스는 알고리즘 군을 정의하고, 일부 필요한 부분만 클라이언트가 구현하도록 한다. 템플릿 메소드가 적용된 모습이다. 하지만 이 코드는 추상 클래스의 구현이 너무 많은 일을 하고 있다. 구현이 많아질수록 하위 클래스는 상위 클래스에 강하게 결합된다. 상위 클래스 내부에 변경이 발생하면 하위 클래스도 변경해야하는 불상사가 벌어진다. 그렇다면 어떻게 이 문제를 해결할 수 있을까?

 

인터페이스에 명시한 메시지로 협력하기

구현에 의존하지 않도록 하면 된다. 객체지향에서 객체는 협력의 문맥에 따라 할당된 책임을 수행하며, 서로의 내부 구현은 모르는 상태로 메시지를 통해 실행에 참여한다. 우리는 생성 책임을 사용 책임으로부터 분리하고 싶은 것이므로, 생성 책임을 별도의 객체에 할당하고 이 객체에게 생성해달라는 메시지를 보내서 사용하면 된다.

 

다만 생성 부분을 보면 N사와 D사가 다른 방식으로 커넥션을 생성해서 사용하고 있다. 그래서 실행 중에 동작하는 커넥션을 받아오려면 N사와 D사의 방식 중 하나를 선택해야 한다. 이때 클라이언트가 구현체(N사 혹은 D사의 커넥션 생성 객체)를 선택하면 구현에 의존하게 되어 변경에 유연하지 못하다. 클라이언트인 UserDao 입장에서는 받아온 커넥션을 사용하기만 하면 되지, 받아오는 커넥션이 어떤 방식으로 생성됐는지는 알 필요가 없다. 그래서 클라이언트는 커넥션을 생성해달라는 메시지만 호출하도록 변경한다. 그리고 이 메시지를 수신할 수 있는 타입을 정의한다. 이 타입은 커넥션 생성 메시지를 공통 인터페이스로 외부에 노출한다. 구현을 어떻게 할 것인지는 이 타입을 구체화한 구현체에서 이루어진다.

 

그대로 적용해보자. 자바는 메시지만 드러내는 도구로 인터페이스를 제공한다. ConnectionMaker가 커넥션 생성 메시지를 수신할 수 있도록 인터페이스를 만들자. N사와 D사는 ConnectionMaker를 구현하여 각자의 방식으로 커넥션을 생성한다. 클라이언트인 UserDao는 ConnectionMaker에게 메시지를 요청해 커넥션을 받아온다.

이제 UserDao는 커넥션 생성으로부터 독립되어 커넥션을 사용할 수 있다. 이렇게 생성 책임을 분리하여 사용 책임만 남은 객체는 의존성을 외부에서 해결해서 넣어줘야하는데, 이를 의존성 주입이라고 한다. 의존성 주입 중 가장 명시적으로 의존성을 드러낼 수 있는 생성자 주입으로 구현을 지정하자.

public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

public class UserDao {
    ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
				// ...
		}
		
		public User get(String id) throws ClassNotFoundException, SQLException {
	      Connection c = connectionMaker.makeConnection();
				// ...
		}
}

public static void main(String[] args) throws ClassNotFoundException, SQLException {
	UserDao dao = new UserDao(new NConnectionMaker());
	// dao 사용
}

정리해보자. 생성과 사용 책임을 분리하면서 기능을 유지하기 위해 개선 과정을 거쳤다. 생성 메시지를 수신할 수 있는 타입을 인터페이스로 정의했고, 구체적인 생성 방식은 인터페이스의 구현체에서 결정했다. 사용 책임만이 남은 클라이언트는 외부로부터 생성 책임 객체의 의존성을 주입받았다. 이제 새로운 커넥션 생성 방식을 추가하더라도 사용 객체에는 영향이 없다. 사용 객체는 커넥션을 생성해달라는 메시지에 의존하고 있지, 커넥션을 어떻게 생성하는지는 모르기 때문이다.

 

OCP

유연한 설계는 기존의 코드를 수정하지 않고 애플리케이션의 동작을 확장할 수 있어야 한다. 컴파일 타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정하면 된다. 이 예제에서 ConnectionMaker라는 추상적인 타입을 두어 달성했다. 변하지 않는 부분인 커넥션 생성 요청은 고정하여 외부에 공개했다. 변하는 부분인 커넥션 초기화 정보는 구현에 숨겼다. UserDao가 추상화에 의존하도록 하면 수정에 대한 영향을 새로운 클래스 생성으로 줄일 수 있다.

 

높은 응집도와 낮은 결합도

응답도는 모듈에 포함된 내부 요소들의 연관성을 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다. 결합도는 두 모듈이 얼마나 많은 정보를 알고 있는지를 나타낸다. 어떤 모듈이 다른 모듈에 대해 꼭 필요한 정보만 갖고 있다면 낮은 결합도를 갖는다. 높은 응집도와 낮은 결합도가 중요한 이유는 설계를 변경하기 쉽게 만들기 때문이다. 객체지향 설계가 추구하는 유연한 아키텍처가 지켜야 하는 특성이다.