본문 바로가기

Spring

[토비의 스프링 3.1 vol.1] 1장. 오브젝트와 의존관계(1)

스프링이 자바에서 가장 중요하게 가치를 두는 것은 바로 객체지향 프로그래밍이 가능한 언어라는 점이다.
잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 바로 스프링의 핵심 철학(👉POJO)이다.

스프링이 가장 관심을 두는 대상은 오브젝트이다.
스프링은 객체지향 설계와 구현에 관해 특정한 모델과 기법을 억지로 강요하지는 않는다.
하지만 오브젝트를 어떻게 효과적으로 설계하고 구현하고, 사용하고, 이를 개선해나갈 것인가에 대한 명쾌한 기준을 마련해준다.

[초난감 DAO]

User

사용자 정보를 저장할 때는 자바빈 규약을 따르는 오브젝트를 이용하면 편리하다.

  • 자바빈 규약
    • 기본 생성자 필수
    • default package가 아닌 지정된 패키지에 저장
    • 멤버 변수의 접근 제어자는 private
    • getter/setter

자바빈

다음 두 가지 관례를 따라 만들어진 오브젝트를 말한다.

  • 디폴트 생성자: 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다.
    툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
    • 리플렉션: 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들을 접근할 수 있도록 해주는 자바 API이다.
      리플렉션을 이용해서 스프링에서 런타임 시 개발자가 등록한 빈을 애플리케이션에서 가져와 사용할 수 있게 되는 것이다.
  • 프로퍼티: 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다.

UserDao

JDBC를 이용하는 작업의 일반적인 순서는 다음과 같다.

  • DB 연결을 위한 Connection을 가져온다
  • SQL을 담은 Statement(또는 PreparedStatement)를 만든다.
  • 만들어진 Statement를 실행한다.
  • 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
  • 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아준다.
  • JDBC API가 만들어내는 exception을 잡아 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.


클래스가 제대로 동작하는지 확인하는 단순한 방법은 DAO의 기능을 사용하는 웹 애플리케이션을 만들어 서버에 배치하고, 웹 브라우저를 통해 DAO 기능을 사용해보는 것이다.
하지만 간단한 UserDao 코드가 동작함을 확인하기 위한 작업치고는 너무 부담이 크다.

main()을 이요한 DAO 테스트 코드

모든 클래스에는 자신을 엔트리 포인트(진입점)로 설정해 직접 실행이 가능하게 해주는 스태틱 메소드 main()이 있다.
이제 main 메소드를 만들고 그 안에서 UserDao의 오브젝트를 생성해서 add()와 get() 메소드를 검증해보면 된다.

[DAO 분리]

관심사의 분리

미래를 준비하는 데 있어 가장 중요한 과제는 어떻게 대비할 것인가이다. 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.
변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있으려면 분리와 확장을 고려한 설계를 해야한다.
변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다.
관심사가 같은 것끼리 모으고 다른 것은 분리해줌으로써 같은 관심사에 효과적으로 집중할 수 있게 만들어주는 것이다.

커넥션 만들기의 추출

UserDao의 관심사항

  • DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심이다.
  • 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것이다.
  • 작업이 끝나면 사용한 리소스의 Statement와 Connection 오브젝트를 닫아줘서 소중한 공유 리소스를 시스템에 돌려주는 것이다.

현재 DB 커넥션을 가져오는 코드는 다른 관심사와 섞여서 같은 get() 메소드에 담겨있다. 더 큰 문제는 get() 메소드에 있는 DB 커넥션을 가져오는 코드와 동일한 코드가 add() 메소드에도 중복되어 있다는 점이다.

하나의 관심사가 방만하게 중복되어 있고, 여기저기 흩어져 있어서 다른 관심의 대상과 얽혀 있으면, 변경이 일어날 때 엄청난 고통을 일으키는 원인이 된다.
지저분하게 꼬여 있는 스파게티 코드가 된다는 뜻이다.

중복 코드의 메소드 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserDao {
    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.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:myaql://localhost/springbook""spring""book");
        return c;
    }
}
cs


변경사항에 대한 검증: 리팩토링과 테스트
리팩토링은 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다.

DB 커넥션 만들기와 독립

상속을 통한 확장
기존에는 같은 클래스에 다른 메소드로 분리됐던 DB 커넥션 연결이라는 관심을 이번에는 상속을 통해 서브클래스로 분리해버리는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract UserDao {
    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ...
    }
 
    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        ...
    }
 
    // 구현 코드는 제거되고 추상 메소드로 바뀌었다
    // 메소드의 구현은 서브클래스가 담당한다.
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}
cs
1
2
3
4
5
6
7
public class NUserDao extends UserDao {
 
    // 상속을 통해 확장된 getConnection() 메소드
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // N사 DB connection 생성 코드
    }
}
cs

슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 디자인 패턴에서 템플릿 메소드 패턴이라고 한다.

상속을 사용하게 되면 단점을 갖게 된다.
상속 자체는 간단해 보이고 사용하기도 편리하게 느껴지지만 사실 많은 한계점이 있다.
자바는 클래스의 다중상속을 허용하지 않기 때문에 다른 목적으로 상속을 적용하기 힘들다
상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다는 점이다.
상속을 통해 관심이 다른 기능을 분리하고, 필요에 따라 다양한 변신이 가능하도록 확장성도 줬지만 여전히 상속관게는 다른 두 가지 관심사에 대해 긴밀한 결합을 허용한다.

[DAO의 확장]

클래스의 분리

새로운 클래스를 만들고 DB 생성 기능을 그 안에 넣는다.
각 메소드에서 매번 오브젝트를 만들수 있지만, 그보다는 한 번만 오브젝트를 만들어 저장해두고 이를 계속 사용하는 편이 낫다.

1
2
3
4
5
6
7
8
// 더 이상 상속을 이용한 확장 방식을 사용할 필요가 없으므로 추상 클래스로 만들 필요가 없다.
public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClssNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook""spring""book");
        return c;
    }
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserDao {
    private SimpleConnectionMaker simpleConnectionMaker;
 
    public UserDao() {
        simpleConnectionMaker = new SimpleConnectionMaker();
    }
 
    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }
 
    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        ...
    }
}
cs

N사와 D사에 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던게 다시 불가능해졌다.
UserDao의 코드가 특정 클래스에 종속되어 있기 때문에 상속을 사용했을때 처럼 UserDao 코드의 수정 없이 DB 커넥션 생성 기능을 변경할 방법이 없다.

클래스를 분리한 경우에도 자유로운 확장이 가능하게 하려면 두가지 문제를 해결해야한다.

  • D사에서 만든 DB 커넥션 제공 클래스는 openConnection()이라는 메소드 이름을 사용했다면 UserDao내에 있는 메소드의 커넥션을 가져오는 코드를 일일이 변경해야 한다.
  • DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야 한다는 점이다.

 

인터페이스의 도입

클래스를 분리하면서 문제를 해결할 수 있는 방법에는 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리를 만들어주는 것이다.
추상화란 어떤 것들이 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업이다.
자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스이다.
인터페이스는 어떤 일을 하겠다는 기능만 정의해놓고 구현방법은 인터페이스를 구현한 클래스들이 알아서 결정할 일이다.

인터페이스를 사용하도록 UserDao를 개선해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserDao {
    private ConnectionMaker connectionMaker;
 
    public UserDao() {
        connectionMaker = new DConnectionMaker();    //구현 클래스 이름이 노출됨
    }
 
    public void add(User user) throws ClassNotFoundException, SQLException {
        // 인터페이스에 정의된 메소드를 사용하므로 이름이 변경될 걱정은 없다.
        Connection c = connectionMaker.makeConnection();
        ...
    }
 
    public User get(String id) throws ClassNotFoundException, SQLException {
        Conection c = connectionMaker.makeConnection();
        ...
    }
}
cs

 

관계설정 책임의 분리

왜 구체적인 클래스를 알아야하는 문제가 발생하는 것일까?
DConnectionMaker()라는 코드는 이 자체만으로도 충분히 독립적인 관심사인 UserDao와 UserDao가 사용할 ConnectionMaker의 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심을 담고 있다.
클라이언트 오브젝트가 제 3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스의 관계를 결정해주는 기능을 두기에 적절하다.
오브젝트 사이의 관계가 만들어지려면 일단 만들어진 오브젝트가 있어야 하는데, 이처럼 직접 생성자를 호출해서 직접 오브젝트를 만드는 방법도 있지만 외부에서 만들어준 것을 가져오는 방법도 있다.

1
2
3
4
5
6
7
8
public class UserDaoTest [
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // UserDao가 사용할 ConenctionMaker구현 클래스를 결정하고 오브젝트를 만든다.
        ConnecctionMaker connectionMaker = new DConnectionMaker();
 
        UserDao dao = new UserDao(connectionMaker);
    }
}
cs


개방 폐쇄 원칙

개방 폐쇄 원칙(OCP, Open-Closed Principle)은 클래스나 모듈은 확정에는 열려 있어야하고 변경에는 닫혀있어야 한다.
UserDao는 DB 연결 방법이라는 기능을 확장하는데는 열려있으며 UserDao에 전혀 영향을 주지 않고도 얼마든지 기능을 확장할 수 있게 되어 있다.
동시에 UserDao 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있으므로 변경에는 닫혀있다고 말할 수 있다.
인터페이스르 사용해 확장 기능을 정의한 대부분의 API는 이 개방 폐쇠 원칙을 따른다고 볼 수 있다.

높은 응집도

응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수 있다.
즉, 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다.

낮은 결합도

책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 즉, 느슨하게 연결된 형태를 유지하는 것이 바람직하다.
느슨한 연결은 관계를 유지하는데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것이다.
결합도란 하나의 오브젝트가 변경이 일어날 때에 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도라고 설명할 수 있다.

전략 패턴

전략패턴은 자신의 기능 context에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.