토비의 스프링 1장

Sihwan Kim·2024년 6월 7일

토비의 스프링

목록 보기
1/5

1장은 초난감 DAO를 리팩토링하는 과정을 통해 Spring에 적용된 개념들을 이해하기 쉽게 설명해주었다.
특히나, IoC/DI/AOP의 개념들은 스프링의 필수 개념임에도 불구하고 정확하게 알지 못한 부분을 쉽게 설명되어 있어서 정말 좋았다.

자세한 코드를 추가하는 것보다 리팩토링할 부분만 작성하는 것이 좋다고 판단되서 리팩토링 중점으로만 작성할 예정이다. 자세한 코드는 책을 구매하거나 다른 github에 자세히 나와 있다.

초난감 UserDao

데이터베이스의 접근해서 User 테이블에 어떤 동작을 하는 UserDao를 매우 난감하게 만드는 걸로 시작한다.

public class UserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {
    
    	//데이터베이스 연결
		Class.forName("oracle.jdbc.driver.OracleDriver");
		Connection c = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "springbook", "springbook"); 

		//사용자 추가 코드

	}

	public User get(String id) throws ClassNotFoundException, SQLException {
    
    	//데이터베이스 연결
		Class.forName("oracle.jdbc.driver.OracleDriver");
		Connection c = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "springbook", "springbook");

		//사용자 불러오는 코드

		return user;

	}

}

지금 당장 보이는 문제점은 add 와 get 함수 각각 안에서 데이터베이스에 연결하는 코드가 작성되어 있다.

이 코드의 문제는 만약 데이터베이스의 주소를 바꿔야한다면 DAO내의 함수 개수만큼 수정해야 한다. 이러한 문제가 발생하는 이유는 분리와 확장을 고려한 설계가 되어 있지않기 때문이다.

관심사의 분리

객체지향의 세계에서는 모든 것이 변한다. 이를 위해 분리와 확장을 고려한 설계를 해야한다.

DB연결 코드 함수 분리 (메소드 추출)

private Connection getConnection() throws ClassNotFoundException, SQLException {
	Class.forName("oracle.jdbc.driver.OracleDriver");
		Connection c = DriverManager.getConnection(
				"jdbc:oracle:thin:@127.0.0.1:1521:xe", "springbook", "springbook");
		return c;
}

DB연결에 대한 메소드를 추출해서 분리하였다. 다만 이렇게 구현을 한다고 하더라도 문제가 발생한다. 만일 주소 혹은 종류가 다른 DB를 동시에 사용한다면 DB마다 함수를 복사해서 새로운 이름으로 작성해주어야 할것이다. 이러한 예시를 이 책에서는 N사와 D사의 DB 연결 차이로 설명한다.

상속을 통한 확장

public abstract class 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;
	// 구현코드는 제거되고 추상메소드로 바뀌었다. 메소드의 구현은 서브클래스가 담당한다.
}

public class DUserDao extends UserDao{
	public Connection getConnection() throws ClassNotFoundException, SQLException{
		// D사 DB Connection 생성코드
	}
}

public class NUserDao extends UserDao{
	public Connection getConnection() throws ClassNotFoundException, SQLException{
		// D사 DB Connection 생성코드
	}
}

템플릿 메소드 패턴

이렇게 상위 클래스에 기본적인 로직을 작성하고, 그 기능의 일부를 서브 클래스에서 필요에 맞게 구현해서 사용하도록 하는 디자인 패턴을 템플릿 메소드 패턴이라고 한다.

즉, UserDao를 상속받아서 getConnection을 추상메소드로 작성해서 서브클래스가 구현할 수 있게 구현한 부분.

팩토리 메소드 패턴

또한 이렇게 서브 클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 것을 팩토리 메소드 패턴이라고 한다.

즉, Connection 클래스의 오브젝트를 어떻게 생성할지를 서브클래스 getConnection에서 결정한 부분.

문제점

상속을 통해서 DB연결 방식에 따라 상속받아서 클래스를 구현할 수 있게 되었다. 하지만 이 방식에는 문제점이 있다. 바로 UserDao를 상속받고 있기 때문에 다른 Dao에 대해서는 사용할 수 없는 함수가 된다. 만일 사용하려면 다른 Dao에도 getConnection 과 같은 함수를 추상메소드로 만들어서 상속으로 구현해야한다.

하지만 그렇게 작성하면 디비가 바뀔 때마다 모든 Dao의 getConnection을 수정하여야 하기 때문에 기존과 달라진 점이 없다.


DAO의 확장

사실 사용자를 추가하고 저장하는 DAO와 데이터베이스 연결은 관심사가 전혀 다르다. 그래서 이제부터는 두개의 관심사를 완전히 독립시키면서 손쉽게 확장할 수 있도록 리팩토링한다.

클래스의 분리

public class UserDao {

	private SimpleConnectionMaker simpleConnectionMaker;
	
	public UserDao() {
		simpleConnectionMaker = new SimpleConnectionMaker();
	}

	// add, get 함수 Connection c = simpleConnectionMaker.makeNewConnection(); 로 커넥션 사용


		
}
public class SimpleConnectionMaker {
	
	public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
		Class.forName("com.mysql.jdbc.Driver");
		
		Connection c = DriverManager.getConnection(
				"jdbc:mysql://localhost/springbook", "spring", "book");
				
		return c;
		
		
	}
}

SimpleConnectionMaker라는 클래스로 분리해서 makeNewConnection() 메소드를 통해 커넥션을 제공도록 구현하였다. 확실하게 클래스 자체가 분리된 것은 깔끔하지만 기존의 문제가 다시 발목을 잡는다.

이렇게 구현된 클래스는 이전의 상속을 통해 해결한 것과 같이 다양한 DB가 생겼을 때를 해결해주지 못한다.

인터페이스 도입

ConnectionMaker

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

public class DConnectionMaker implements ConnectionMaker {

	@Override
	public Connection makeConnection() throws ClassNotFoundException, SQLException {
		// D사만의 방법
	}
}

UserDao

public class UserDao {

	private ConnectionMaker connectionMaker;
	
	
	public UserDao() {
		connectionMaker = new DConnectionMaker();
	}

	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = connectionMaker.makeConnection();
        //사용자 추가 코드
	}

인터페이스를 제공함으로써 UserDao는ConnectionMaker가 사용될 것이라고만 알면되며 ConnectionMaker를 어떻게 구현하던지 makeConnection() 메소드가 커넥션을 반환해준다는 보장을 얻을 수 있다.

하지만 여전히 문제가 생긴다.

UserDao는 DB연결의 관심이 없어야 정상이다. 인터페이스를 통해 어떻게 구현될지 몰라도 되는 것이 목표였다.

하지만, 생성자에서 connectionMaker를 생성할때 여전히 어떤 구현체의 인스턴스로 넣을지 알아야한다. 이는 완전히 분리되어 있다고 할 수도 없으며, 디비 연결이 변경되면 UserDao를 수정해야 한다.

관계설정 책임 분리

UserDao가 connectionMaker의 구현체를 알아야하는 책임을 분리해서 떠넘긴다.

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

	public static void main(String[] args) throws ClassNotFoundException, SQLException{
		
		ConnectionMaker connectionMaker = new DConnectionMaker();
		
		UserDao dao = new UserDao(connectionMaker);
		
	}
}

이렇게 클라이언트로 ConnectionMaker의 구현체를 알 책임을 떠넘겼다. 사실 클라이언트는 이 책임을 가질 이유가 없다고 생각하는 찰나 다음장에서 바로 언급하고 해결한다.

아무튼 우선 UserDao가 가지는 책임을 분리시켜서 DB연결에 대한 아무런 책임도 갖지 않도록 리팩토링하였다.

개방 폐쇄 원칙 (Open-Closed Principle)

객체지향의 SOLID 원칙중 O를 담당하는 원칙이다. 이는 클래스나 모듈은 확장에는 열려있어야하고, 변경에는 닫혀있어야 한다는 원칙이다.

초난감 UserDao는 DB연결 방법을 확장하기에도 불편하며, 확장하고자 한다면 Dao내부 코드가 변경되니 변경에도 닫혀있지 않았기 때문에 문제가 발생한다.

높은 응집도와 낮은 결합도

응집도가 높다는 건 하나의 모듈, 클래스가 하나의 책임 또는 관심에만 집중되어 있다는 뜻이고,

낮은 결합도는 책임과 관심사가 다른 오브젝트, 모듈과는 느슨하게 연결되어 있다는 뜻이다.
SOLID의 S와 같은 맥락으로 생각된다.

응집도를 높이고 결합도를 낮추지 않으면, 변경에 따른 작업량이 많아지고, 변경으로 인해 버그가 발생할 가능성이 높아진다. (DB 주소를 모든 함수에서 바꿔주어야 했던 것 처럼.)

전략 패턴

개방 폐쇄 원칙의 실현에 가장 잘 들어 맞는 패턴으로, 필요에 따라 변경이 필요한 알고리즘 클래스를 인터페이스를 통해 통째로 외부로 분리시키고,

이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴.

즉, 어떤 데이터베이스를 사용하는 지에 따라 달라지는 ConnectionMaker 클래스를 인터페이스로 분리하고 이를 구현한 구체적인 클래스를 필요에 따라 바꿔서 사용하는 패턴.

문제점

앞서 말했던 것 처럼 현재는 책임을 가지지 않아야할 UserTest가 디비연결에 대한 책임을 가지고 있다. 이제 UserTest로 넘겼던 책임을 실제 책임이 있는 곳으로 넘겨야겠다.


팩토리로 변경

UserTest는 DB연결에 대한 책임을 가질 필요가 없다. 정확히는 가지면 안된다. 심지어 UserDoa를 생성하는 책임 마저도 가질 이유가 없다.

팩토리 구현

그래서 Dao를 생성하는 책임을 가질 DaoFactory를 만든다.

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(getConnectionMaker());
    }
}

private DSimpleConnectionMaker getConnectionMaker() {
        return new DSimpleConnectionMaker();
    }

사용

public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {

        UserDao dao = new DaoFactory().userDao();

팩토리를 통해 userDao의 생성에 대한 책임을 넘겼다.


IoC 제어의 역전

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

관계설정 책임을 분리하게 되면서 UserDao가 사용할 ConnectionMaker를 UserDao가 선택하지 않게 되었다. 이처럼 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하는 것을 IoC 제어의 역전이라고 한다.

라이브러리와 프레임워크 차이

프레임워크도 제어의 역전 개념이 적용된 기술이다.
하지만 라이브러리는 그렇지 않다. 이것이 가장 큰 차이이자 핵심이다. 이 책을 읽기 전까지는 나도 이 개념을 헷갈리고 있었다.

라이브러리는 코드를 작성하면서 필요한 기능이 있을 때 사용할 뿐이지만, 프레임워크는 흐름을 주도하는 중에 개발자가 만든 코드를 사용하도록 만든다. 즉 제어가 역전되어 있는 것이다.

애플리케이션 컨텍스트와 빈 팩토리

스프링 빈(Bean)

스프링에서 스프링이 제어권을 가지고 관계를 부여하는 오브젝트를 빈이라고 한다. 스프링 빈은 스프링 컨테이너가 생성, 사용등을 제어하는 제어의 역전이 적용된 오브젝트이다.

빈 팩토리 (Bean Factory)

스프링 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory)라고 부른다.

어플리케이션 컨텍스트 (Application Context)

어플리케이션 컨텍스트는 빈 팩토리를 확장한 개념으로, 빈 팩토리 기능과 스프링 어플리케이션 전반에 걸친 모든 구성요소의 제어 작업을 담당한다.

IoC 컨테이너, 스프링 컨테이너

모두 빈 팩토리와 어플리케이션 컨텍스트를 의미하는데, 주로 스프링 컨테이너는 어플리케이션 컨텍스트를 IoC컨테이너는 빈 팩토리를 이야기한다.

UserDao 빈 등록

UserDao의 제어권을 스프링 프레임워크로 넘기기 위해서 UserDao를 Bean으로 등록하였다.

@Configuration // `애플리케이션 컨텍스트` 혹은 `빈 팩토리`가 사용할 설정 정보라는 표시
public class DaoFactory {

    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드
    public UserDao userDao() {
        return new UserDao(getConnectionMaker());
    }

    @Bean // 오브젝트 생성을 담당하는 IoC용 메소드
    public DSimpleConnectionMaker getConnectionMaker() {
        return new DSimpleConnectionMaker();
    }
}

사용

public class UserDaoTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDao userDao = applicationContext.getBean("userDao", UserDao.class);

UserDao의 생성과 UserDao가 사용하는 인스턴스에 대한 모든 제어를 스프링이 가져가면서 userDao를 불러올 때 어플리케이션 컨텍스트에서 불러올 수 있게 구현되었다.

어플리케이션 컨텍스트 동작 방식

@Configuration 이 붙은 클래스를 설정정보로 인식하며, 내부에 @Bean이 붙은 메소드의 이름을 가져와 빈 목록들을 만든다. 이렇게 어플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점들은 다음과 같다.

클라이언트가 구체적인 팩토리 클래스를 알 필요가 없다.

기존의 DaoFactory방식은 DaoFactory 클래스를 알아야 이를 통해 userDao를 받아올 수 있었다. 하지만 Bean으로 등록해서 어플리케이션 컨텍스트를 사용하면 Bean이름만알면 원하는 Dao를 불러올 수 있게 되었다.

어플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.

ConnectionMaker와 같은 사용하는 오브젝트에 대한 관계설정 뿐만아니라 오브젝트가 만들어지는 시점, 방식 전략등을 제공한다. 또한 자동생성, 후처리 등등 다양한 기능을 제공한다.


싱글톤 레지스트리로서의 어플리케이션 컨텍스트

사실 기존 DaoFactory의 문제점이 한가지 더 있었다.

public class DaoFactory {
    public UserDao userDao() {
        return new UserDao(getConnectionMaker());
    }
}
UserDao dao = new DaoFactory().userDao();

위와 같은 코드가 적용되면 new 로인해서 UserDao가 요청시마다 인스턴스가 생성된다. 이렇게 동작하면 가비지 컬렉션의 성능이 좋아졌어도 서버가 감당하기 힘들어진다. 그래서 대부분 멀티스레드 환경에서 싱글톤으로 동작한다.

싱글톤의 문제

  • private 생성자를 갖고 있기 때문에 상속이 불가능하다.
  • 테스트를 위한 Mock 오브젝트를 대체하기가 어려워 테스트가 어렵다.
  • 클래스 로더 구성에 따라 싱글톤이 보장되지 않을 수 도있다.
  • 싱글통의 스태틱 메소드를 이용해서 불러오기 때문에 전역의 문제가 생긴다.

싱글톤 레지스트리

싱글톤을 구현하는 것에 있어서 다양한 문제가 존재하기 때문에 스프링이 직접 싱글톤 형태의 오브젝트를 만들과 관리하는 기능을 제공하는데 이를 싱글톤 레지스트리라고 한다.

즉, 스프링은 IoC 컨테이너일 뿐 아니라 싱글톤 레지스트리이다.


의존성 주입(DI)

스프링이 IoC 방식에서 다른 프레임워크와 차별화돼서 제공하는 기능을 설명하기 위해 DI(Dependency Injection)라는 용어가 탄생하였다. 스프링의 IoC를 DI라고 이해하면 쉬울 것 같다.

의존관계 주입의 세가지 조건

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 즉, 인터페이스에만 의존하고 있어야 한다.

  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.

  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다.

이때 핵심은 의존관계를 맺도록 도와주는 제 3의 존재가 있다는 것이다. 스프링에서는 어플리케이션 컨텍스트, 빈팩토리 등이 이런 역할을 한다.

의존관계 검색과 주입

스프링에서 제공하는 IoC 방법에는 DI만 존재하는 것은 아니다. DL(Dependency LookUp) 즉, 의존관계 검색도 존재한다.

DI와 차이점은 자신에게 필요한 의존성을 주입하는 것이 아닌 직접 찾는 것이다.

public class UserDao {
    ConnectionMaker connectionMaker;
    
    public UserDao() {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(DaoFactory.class);

        this.connectionMaker = applicationContext.getBean(ConnectionMaker.class);
    }

이렇게 UserDao에서 필요한 ConnectionMaker를 검색해서 UserDao를 생성하는 방식이다.

물론, DI가 DL에 비해 간편하고 좋지만, DL을 꼭 써야하는 환경이 있다. 바로 테스트 코드이다. 테스트코드에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.(스프링 부트에서는 SpringBootTest 어노테이션의 힘으로 가능하긴 한것 같지만...)

또한 DI는 자기 자신도 빈 오브젝트여야 하지만, DL은 자기 자신은 빈에 등록될 필요가 없다. DI를 받으려면 자기 자신도 빈에 등록되어야 한다는 것을 잊지 말자.

메소드를 통한 의존관계 주입

지금까지는 생성자에 포함된 의존관계를 주입해주었지만 아래와 같이 메소드를 통해서도 의존관계를 주입해줄 수있다.

public class UserDao{
	private ConnectionMaker connectionMaker;
    
    public void setConnectionMaker(ConnectionMaker connectionMaker){
    	this.connectionMaker = connerctionMaker;
    }
}

생성자를 통해 주입하는 경우에는 많은 파라미터를 한번에 주입할 수 있다는 장점이 있지만, 같은 타입을 여러개 받을 경우 실수를 줄이기 위해서는 Setter를 통해 주입하는 것이 좋을 수있다.

Property값의 주입

우리가 JPA를 사용할 때 Properties파일에 데이터베이스관련 정보를 넣어주면 자동으로 DB설정이 완료되는 것도 setter를 통한 의존관계주입이 사용된 것이다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/db serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: root
    password: 

이런식으로 설정을 구성하면

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriverClass(com.mysql.cj.jdbc.Driver);
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/db ");
        dataSource.setUsername("root");
        dataSource.setPassword("");
        return dataSource;
    }

이렇게 Setter를 통해 dataSource의 의존성을 주입한다.


1장 느낀점

생각보다 스프링이 제공해주는 DI, IoC, 데이터베이스 의존성 주입 등등. 그냥 막연하게 자동으로 된다고 생각했던 것을 반성하게 되었다.

다른 책들을 읽을 때에는 개념에 대해 설명하여도 "그래서 이걸 왜 쓰는데?"하는 의문으로 인터넷을 뒤져봐야했지만, 이책은 개념만 설명하는 것이 아닌 실제 왜 필요한지가 같이 있어서 지금까지 매우 흥미롭게 읽히는 것 같다.

0개의 댓글