[Spring]토비의 스프링3.1 - 1장

sally·2021년 7월 14일
0
post-thumbnail

1장에서는 스프링의 오브젝트의 설계와 구현, 동작원리를 초난감 DAO를 리팩토링 하면서 차근차근 풀어준다.
토비의 스프링을 읽고 난 뒤의 내용을 정리해서 써보겠다!


🤷‍♀️초난감 DAO와 리팩토링

유저 데이터를 추가, 조회 하기 위해서 다음과 같은 간단한 DAO를 만들어보자.

DAO
DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

public class UserDao {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws SQLException, ClassNotFoundException {
        Class.forName("org.mariadb.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mariadb://localhost:3306/toby","scott","1234");

        PreparedStatement ps = c.prepareStatement("insert into user(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 SQLException, ClassNotFoundException {
        Class.forName("org.mariadb.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mariadb://localhost:3306/toby","scott","1234");

        PreparedStatement ps = c.prepareStatement("select * from user 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;
    }
}

테스트를 진행해보면 insert, select 모두 잘 동작하는 코드다. 하지만 실제로 이렇게 개발을 했다가는 코드를 보는 모든 사람들을 당황하게 할 수 있는 초난감 코드의 조건을 모두 갖춘 DAO 코드다.

💥 위의 DAO 코드에는 여러 관심사항이 한 코드에 존재하게 된다.

  • DB 연결을 위한 커넥션을 어떻게 가져올지?
  • SQL 문장을 담을 statement를 만들고 실행하는 것
  • 사용한 리소스를 닫아주는 것

이렇게 한 코드에 여러 관심사항이 존재하게 되면 많은 중복 코드가 발생하고, 코드가 지저분해지고, 변경 대응에 취약하다.

DAO의 분리

중복 코드의 메소드 추출

커넥션을 가져오는 중복된 코드를 분리해서 DB 연결 코드를 독립적인 메소드로 만들어 준다.

    private Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.mariadb.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mariadb://localhost:3306/toby","scott","1234");

        return c;
    }

메소드 추출로 변화에 좀 더 유연하게 대처할 수 있는 코드를 만들었다.

상속을 통한 확장

메소드 추출로 변화에 대응하는 수준의 코드를 만들었다면, 이번엔 변화를 반기는 DAO를 만들어보자.

만약 위의 코드를 여러 고객에게 제공하고, UserDao는 제공하고 싶지 않다면 어떻게 해야 할까?

public abstract class UserDao_Extends {

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

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

        Connection c = getConnection();
		
        ...
    }
	//구현 코드는 제거하고 메소드로 바꿈
    //메소드 구현은 서브 클래스가 담당
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

DB 커넥션에 대한 메소드를 추상 메소드로 제공함으로써, UserDao에 대한 코드를 제공하지않고 DB 커넥션 메소드를 원하는 방식으로 확장한 후에 사용할 수 있다.

위의 코드에는 템플릿 메소드 패턴팩토리 메소드 패턴에 대한 개념이 적용되어있다.

💛노란색 동그라미가 템플릿 메소드 패턴 💗분홍색 동그라미가 팩토리 메소드 패턴이 적용된 부분이다.

템플릿 메소드 패턴

  • 슈퍼 클래스에서 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의해두고 서브 클래스에서는 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법
  • 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법

팩토리 메소드 패턴

  • 객체를 만들어내는 부분을 서브클래스에 위임한다
  • 즉 서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 방법
  • 템플릿 메소드 패턴과 비슷하게 상속을 통해 기능을 확장하게 하는 패턴

💥 하지만 상속을 사용했다는 것엔 여러 단점이 존재한다.

  • 자바는 다중 상속이 불가능하다.
  • 슈퍼클래스 내부 변화가 있을 때 서브클래스들도 다 수정해야 한다.(너무 밀접하다)
  • DB 커넥션 생성 코드를 다른 DAO 클래스에 적용할 수 없다.

DAO의 확장

클래스의 분리

public class UserDao_Class{
    private SimpleConnectionMaker simpleConnectionMaker;

    //한번만 만들어 인스턴스 변수에 저장해두고 메소드에서 사용하게 함
    public UserDao_Class(){
        simpleConnectionMaker = new SimpleConnectionMaker();
    }
    
    public void add(User user) throws SQLException, ClassNotFoundException {
        Connection c = simpleConnectionMaker.makeNewConnection();

		...
    }

    public User get(String id) throws SQLException, ClassNotFoundException {
        Connection c = simpleConnectionMaker.makeNewConnection();

		...
    }
}
public class SimpleConnectionMaker {
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("org.mariadb.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mariadb://localhost:3306/toby","scott","1234");
        return c;
    }
}

DB커넥션과 관련된 부분을 독립적인 클래스로 만들어서 상속을 제거하였다.

💥 하지만 아직 코드에서 여러가지 단점이 존재한다.

  • UserDao가 DB 커넥션을 가져오는 구체적인 방법에 종속되어 버렸다(UserDao의 수정이 불가피해졌다)
  • 결국 다시 DB 커넥션을 가져오는 방법에 대한 확장이 다시 힘들어졌다(이럴거면 상속을 왜 제거한거지?).

인터페이스의 도입

public class UserDao_Interface {
    //인터페이스를 통해 오브젝트에 접근
    private ConnectionMaker connectionMaker;

    public UserDao_Interface(){
        connectionMaker = new DConnectionMaker();
    }

    public void add(User user) throws SQLException, ClassNotFoundException {
        //인터페이스에 정의된 메소드를 사용하므로 클래스가 바뀌어도 메소드 이름이 변경되지 않음
        Connection c = connectionMaker.makeConnection();

		...
    }

    public User get(String id) throws SQLException, ClassNotFoundException {
        Connection c = connectionMaker.makeConnection();

		...
    }
}
public class DConnectionMaker implements ConnectionMaker{
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        //D사의 독자적인 방법으로 Connection을 생성하는 코드
        Class.forName("org.mariadb.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mariadb://localhost:3306/toby","scott","1234");
        return c;
    }
}
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;

}

추상화

  • 어떤 것들의 공통적인 성격을 뽑아내어 이를 따로 분리해내는 작업
  • 자바가 추상화를 위해 제공하는 가장 유용한 도구는 바로 인터페이스

인터페이스로 오브젝트에 접근하면 사용할 클래스가 무엇인지 몰라도 된다. 즉 인터페이스를 통해 접근하면 실제 구현 클래스를 바꿔도 신경을 안써도 된다!

💥 하지만 여전히 UserDao는 DB 커넥션을 가져오는 구체적인 방법에 종속되어 있다.

connectionMaker = new DConnectionMaker();

관계설정 책임의 분리

public class UserDao_Relation {

    private ConnectionMaker connectionMaker;
    //수정한 생성자
    public UserDao_Relation(ConnectionMaker connectionMaker){
        this.connectionMaker = connectionMaker;
    }

}
@SpringBootTest
class UserDaoTest_Relation {

    @Test
    void userDaoTest() throws SQLException, ClassNotFoundException {
        //UserDao가 사용할 ConnectinMaker 구현 클래스를 결정하고 오브젝트 생성
        ConnectionMaker connectionMaker = new DConnectionMaker();
        //사용할 ConnectionMaker 타입의 오브젝트 제공, 결국 두 오브젝트 사이의 의존관계 설정 효과
        UserDao_Relation userDao = new UserDao_Relation(connectionMaker);

	...
    }

}

connectionMaker = new DConnectionMaker(); 코드 자체가 UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이라는 관심사를 담고있다. 관계 설정에 대한 관심

런타임 오브젝트 관계를 갖는 구조를 클라이언트의 책임으로 옮겨 주었다.

앞의 방법들보다 더 깔끔하고 유연해진 방법으로 클래스를 분리하였으며, 서로 영향을 주지도 않으면서 자유롭게 확장할 수 있는 구조가 되었다.
UserDao는 이제 자신의 관심사(SQL 생성 및 실행)에만 집중할 수 있게 되었다.

💥 UserDaoTest는 테스트를 하려고 만든 것이다. 하지만 결국 테스트관계설정의 책임까지 떠맡게 되었다.

원칙과 패턴

수정에 수정을 거듭한 UserDao개방 폐쇄의 원칙을 따르고 있으며, 응집력은 높고 결합도는 낮으며, 전략 패턴을 적용한 코드라고 할 수 있다.

개방 폐쇄 원칙

  • '클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다'
  • 지금까지 수정한 UserDao는 DB 연결 방법이라는 기능을 확장할수 있게 되었다. 동시에 UserDao 핵심 기능 구현 코드는 변경에는 영향을 받지 않고 유지(인테페이스를이용)할 수 있기 때문에 변경에는 닫혀있다.

높은 응집도와 낮은 결합도

  • 응집도가 높다 라는 것은 하나의 모듈, 클래스가 하나의 관심사에만 집중되어 있다는 뜻이다.
    UserDao클래스는 그 자체로 자신의 책임에 대한 응집도가 높다. ConnectionMaker또한 자신의 순수한 자신의 책임을 담당하는데만 충실할 수 있다.
  • 결합도가 낮다라는 것은 다른 오브젝트,모듈과 느슨하게 연결된 형태를 말한다. 결합도가 낮아지면 변화에 대응하는 속도가 높아지고 구성이 깔끔해진다. 또한 확장하기에도 매우 편리해진다.
    ConnectionMakerUserDao 는 인터페이스를 통해 매우 느슨하게 연결되어 있기 때문에 낮은 결합도로 최소한으로 연결되어 있다.

전략 패턴

  • 자신의 기능 맥락(Context)에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 외부로 분리시키고, 이를 구현한 구체적인 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
  • UserDao는 전략 패턴의 컨텍스트에 해당한다. 컨텍스트(UserDao)를 사용하는 클라이언트(UserDaoTest)는 컨텐스트가 사용할 전략(ConnectionMaker를 구현한 클래스, 예를 들어 DConnectionMaker)을 컨텍스트의 생성자를 통해 제공한다.

제어의 역전(IoC)

오브젝트 팩토리

public class DaoFactory {
    public UserDao_Relation userDao(){
        //팩토리의 메소드는 UserDao타입의 오브젝트를 어떻게 만들고, 어떻게 준비시킬지 결정한다.
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao_Relation userDao = new UserDao_Relation(connectionMaker);
        return userDao;
    }
}
@SpringBootTest
class UserDaoTest_Factory {

    @Test
    void userDaoTest() throws SQLException, ClassNotFoundException {
        UserDao_Relation userDao = new DaoFactory().userDao();
        
		...
    }

}

팩토리 메소드를 적용해 Daofactory를 분리함으로써 애플리케이션 컴포넌트 역할을 하는 오브젝트(실질적인 로직) 와 애플리케이션 구조를 결정하는 오브젝트를 분리했다.

public class DaoFactory_ObjectFactory {
    public UserDao_Relation userDao(){
        return new UserDao_Relation(connectionMaker());
    }
    
    public ConnectionMaker connectionMaker(){
        //분리해서 중복을 제거한 ConnectionMaker 타입 오브젝트 생성 코드 
        return new DConnectionMaker();
    }
}

ConnectionMaker의 구현 클래스를 결정하고 오브젝트를 만드는 코드를 별도의 메소드로 뽑아내었다.
초난감 DAO 코드에서 getConnection 메소드를 분리해낸 것과 동일한 리팩토링 방법이다.

제어권의 이전을 통한 제어관계 역전

제어의 역전

  • 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다.
  • 간단하게 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다.
  • 제어권을 상위 템플릿 메소드에 넘기고 자신은 필요할 때 호출되어 사용되도록 한다는 개념이다.

우리가 만든 UserDaoDaoFactory에도 제어의 역전이 적용되어 있다. 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 DaoFacotry를 도입했던 과정이 바로 IoC를 적용하는 과정이였다.

  • UserDao는 어떤 ConnectionMaker를 사용할지가 DaoFactory에 의해 정해진다.

  • UserDaoTestDaoFactory가 만들고 초기화하여 리턴해주는 DAO만 사용할 수 있다.

스프링의 IoC

오브젝트 팩토리를 스프링이 관리하는 오브젝트로 변경하여 사용해보자.

앞서 몇개의 용어 정리가 필요하다.

bean : 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
bean Factory: 제어(bean의 생성과 관계설정)를 담당하는 IoC 오브젝트
application context : BeanFactory를 확장한 컨테이너. 스프링의 각종 부가 서비스를 빈 팩토리에 얹었다고 보면 된다. 위의 DaoFactory(설계도)가 여기에 해당한다.

annotaion을 사용하여 DaoFactory를 application context의 설정정보로 사용해보자

//애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
@Configuration
public class DaoFactory_Spring {
	//오브젝트 생성을 담당하는 IoC용 메소드라는 표시
    @Bean
    public UserDao_Relation userDao(){
        return new UserDao_Relation(connectionMaker());
    }
    
    @Bean
    public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
    }
}
@SpringBootTest
class UserDaoTest_AppContext {

    @Test
    void userDaoTest() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao_Relation userDao = context.getBean("userDao",UserDao_Relation.class);

		...
    }

}

스프링을 적용하긴 했지만 앞에서 만든 DaoFactory를 직접 사용한 것과 기능적으로는 다를바가 없다. 오히려 더 번거로운 준비 작업과 코드가 필요하다.

하지만 스프링을 사용하게되면, 애플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점이 많다.

  • 어플리케이션 컨텍스트를 이용하여 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
  • 빈을 검색하는 다양한 방법을 제공한다.

싱글톤 레지스토리와 오브젝트 스코프

싱글톤 패턴
클래스 애플리케이션 내에서 인스턴스의 갯수를 하나로 존재하도록 강제하는 패턴

디자인 패턴중에서 가장 자주 활용되는 패턴이기도 하지만 가장 많은 비판을 받는 패턴이기도 하다.
💥싱글톤 패턴은 문제점이 많기 때문이다..!!

  • private 생성자 -> 상속 불가
  • 테스트의 어려움
  • 1개만 있도록 통제하기 어려움
  • global state로 사용되기 쉬움 --> 객체지향 프로그래밍에서 지양하는 모델

싱글톤 레지스트리

자바의 기본적인 싱글톤 패턴의 구현 방식은 여러가지 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 자바 클래스를 싱글톤으로 사용하면서 싱글톤 패턴의 장점만 가져올 수 있다. 그것이 바로 싱글톤 레지스트리이다.

의존관계 주입(DI)

의존관계란?


의존관계는 항상 방향성이 있어야 한다. 위의 그림은 A가 B에 의존하고 있음을 나타낸다.
즉 다르게 말하면 B는 A의 변화에 영향을 받지 않는다는 것을 뜻하기도 한다.


지금까지 구현했던 UserDao를 UML로 표현했다. UserDaoConnectionMaker에 의존하고 있다. 따라서 ConnectionMaker가 변하낟면 영향을 직접적으로 바뀐다면 UserDao는 영향을 받게 되지만 구현 클래스인 DConnectionMaker등의 변화에는 영향을 받지 않는다.

인터페이스에 대해서 의존관계가 만들어지면 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 된다.

런타임시에 오브젝트 사이에서 만들어지는 의존관계도 있는데, 클래스와 인터페이스를 통해 드러나는 의존관계로는 런타임 의존관계를 파악할 수 없다.

의존관계 주입은 의존 오브젝트와 그것을 사용할 오브젝트(클라이언트)를 런타임에 연결해 주는 작업이다.

즉, DI는 다음 세 조건을 충족해야 한다.

  • 클래스 다이어그램 및 코드에는 런타임 시점의 의존관계가 드러나지 않는다 -> 인터페이스에만 의존

  • 런타임 시점의 의존관계는 제3자가 결정한다. (ex: 컨테이너, 팩토리)

  • 의존관계는 외부에서 주입함으로써 만들어진다. (ex: 컨테이너, 팩토리)

단, 외부에서 파라미터로 오브젝트를 넘겨줬다고 해서, 즉 주입했다고 해서 다 DI가 아니다!!
DI에서 말하는 주입은 다이내믹하게 구현 클래스를 결정해서 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이루어져야 한다.

🎇 '설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다.'🎇

의존관계 검색(DL)과 주입

의존관계 검색(DL)
자신이 필요로 하는 오브젝트를 능동적으로 찾는다.
오브젝트의 생성 작업은 외부 컨테이너에게 맡기지만, 이를 가져올때는 메소드나 생성자 주입 대신에 스스로 컨테이너에 요청한다.

DI🙆‍♀️ vs DL🙅‍♂️
일반적으로 DI가 훨씬 단순하고 깔끔하다. DL은 코드 안에 오브젝트 팩토리나 스프링 API가 나타나기 때문에 바람직하지 않다.
하지만 DL를 사용해야 할 때가 있다. ex) main()메소드에서 스프링 컨테이너에 담긴 오브젝트를 사용해야 할때!

DI와 DL의 차이점 : DI 받기 위해선 본인도 @Bean이어야 한다.


마치며

스프링이란 '어떻게 오브젝트라 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크'라는 사실을 꼭 기억하자.
스프링을 사용한다고 좋은 객체지향 설계와 유연한 코드가 저절로 만들어지는건 아니지만, 스프링은 그런 설계와 코드를 적용하고자 할 때 좋은 동반자가 되어줄 것이다. 💖

profile
Believe you can, then you will✨

0개의 댓글