본문 바로가기
도서기록/토비의 스프링

1장 오브젝트와 의존관계

by 코엘리 2025. 3. 8.
반응형

1.1 초난감 DAO

  • DAO(Data Access Object)는 DB 를 사용해서 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를 말한다.

1.1.1

public class User {
    String id;
    String name;
    String password;

    (getter/setter method)

User 오브젝트에 담긴 정보가 실제로 보관될 DB 테이블은 아래와 같다.

create table users (
    id varchar(10) primary key,
    name varchar(20) not null,
    password varchar(20) not null

1.1.2 UserDao

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

  1. DB 연결을 위한 Connection 을 가져온다.
  2. SQL 을 담은 Statement(또는 PreparedStatement)를 만든다.
  3. 만들어진 Statement를 실행한다.
  4. 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다.
  5. 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 닫아준다.
  6. JDBC API 가 만들어내는 예외를 처리하거나 던진다.

아래는 완성된 UserDao 이다.

public class UserDao {

  public void add(User user) throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver");
    Connection c = DriverManager.getConnection(
        "jdbc:mysql://localhost/springbook", "spring", "book");
    PreparedStatement ps = c.prepareStatement(
        "insert into users(id, name , password) values(ζ? ,?)");
    ps.setString(1, user.getld());
    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.jdbc.Driver");
    Connection c = DriverManager.getConnection(
        "jdbc:mysql://localhost/springbook", "spring", "book");
    PreparedStatement ps = c.prepareStatement(
        "select * from users where id = ?");
    ps.setString(1, id);
    ResultSet rs = ps.executeQuery();
    rs.next();
    User user = new User();
    user.setld(rs.getString("id"));
    user.setName(rs.getString("name"));
    user.setPassword(rs.getString(password"));
    rs.close();
    ps.close();
    c.close();
    return user;
  }
}

위 코드는 문제가 많고 이를 객체지향적으로 설계하는 방법을 설명한다.

1.2 DAO의 분리

1.2.1 관심사의 분리

  • 개발자가 객체를 설계할 때 가장 염두해 둬야 하는 사항은 미래의 변화를 어떻게 대비할 것인가이다.
  • 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것이다.
  • 모든 변경과 발전은 한 번에 한 가지 관심사항에 집중해서 일어난다. 따라서, 우리는 한 가지 관심이 한 군데 집중되게 하는 것이 중요하다.

1.2.2 커넥션 만들기의 추출

UserDao의 관심사항은 총 세가지다.

  1. DB와 연결을 위한 커넥션
  2. SQL 문장을 담을 Statement를 담고 실행하는 것
  3. 작업이 끝난 리소스를 돌려주는 것

중복 코드 메소드 추출

가장 먼저 중복 코드를 분리한다.

public class UserDao {

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

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

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

변경사항에 대한 검증: 리팩토링과 테스트

  • 공통 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 extract method 기법이라고 한다.
  • 리팩토링: 기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업

1.2.3 DB 커넥션 만들기의 독립

DB 커넥션을 가져오는 방법이 종종 변경될 가능성이 있다.
따라서, 고객 스스로 원하는 DB 커넥션 생성방식을 적용해가며 UserDao를 사용할 수 있게 해보자.

상속을 통한 확장

  • getConnection() 을 추상메서드로 만들고, UserDao 클래스를 각각 상속한 자식(서브) 클래스(NUserDao, DUserDao)에서 이를 구현한다.

    public abstract class UserDao {
    
    public void add(User user) throws ClassNotFoundException, SQLException {
      getConnection();
      ...
    }
    
    public User get(String id) throws ClassNotFoundException, SQLException {
      getConnection();
      ...
    }
    
    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
    }
public class NUserDao extends UserDao {
  public Connection getConnection() throws ClassNotFoundException, SQLException {
    // N사 DB connection 생성코드
  }
}

public class DUserDao extends UserDao {
  public Connection getConnection() throws ClassNotFoundException, SQLException {
    // D사 DB connection 생성코드
  }
}
  • 슈퍼 클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩 가능한 protected 메소드 등으로 만든 뒤, 서브 클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법을 디자인 패턴에서는 템플릿 메소드 패턴이라고 한다.
  • 서브클래스에서 구체적인 오브젝트 생성방법을 결정하게 하는 것을 팩토리 메서드 패턴이라고 한다.

상속의 문제점

  • 만약 UserDao가 다른목적을 위해 상속을 사용하고 있다면, 혹은 상속을 사용해야 한다면 자바는 클래스의 다중 상속을 허용하지 않기 때문에 문제가 생긴다.

1.3 DAO의 확장

1.3.1 클래스의 분리

  • UserDao는 어떤 테이블을 사용해 어떤 SQL을 만들 것인가, 어떤 오브젝트를 DB 와 주고받을 것인가와 같은 관심을 가진 코드를 모아두었다.
  • NUserDao, DUserDao는 DB 연결 방식이나 커넥션의 관심을 모아두었다.
  • 관심사가 다르고 변화의 성격이 다른 이 두가지 코드를 좀 더 분리하여 별도의 클래스에 담아본다.
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();
    ...
  }
}

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;
  }
}
  • 위와 같이 변경했을 때 N사와 D사 사이 DB 커넥션 기능 확장 사용이 다시 불가능해지는 문제점이 발생한다.
  • 근본적인 원인은 DB 커넥션을 가져오는 클래스에 대해 UserDao 가 너무 많이 알고 있기 때문이다.

1.3.2 인터페이스의 도입

  • 추상화란 어떤 것들의 공통적인 성격을 뽑아내어 이를 분리해내는 작업이다.
  • 인터페이스를 통해 UserDao 가 관심이 있는 기능에만 관심을 가지도록 변경해본다.

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 {
    Connection c = connectionMaker.makeConnection();
    ...
  }
}

위와 같이 인터페이스를 도입하였을 때, DConnection 클래스의 생성자를 호출해서 오브젝트를 생성하는 코드가 여전히 UserDao 에 남아있는 것을 확인할 수 있다.

1.3.3 관계설정 책임과 분리

  • UserDao가 어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지 결정(관계를 설정해주는)하는 또 다른 분리된 클래스가 필요하며, 이는 UserDao를 사용하는 클라이언트 오브젝트가 될 수 있다.
  • 이렇게 제 3의 클래스로 관계를 설정하게 되면 오브젝트 사이의 관계가 만들어진다고 할 수 있는데, 이는 클래스 사이 관계와는 다르다.
    • 클래스 사이 관계는 코드에 다른 클래스 이름이 나타나기 때문에 만들어지는 것이다.
    • 오브젝트 사이 관계는 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다.
public class UserDao {

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

public class UserDaoTest {
  public static void main(String[] args) throws SQLException {
    ConnectionMaker connectionMaker = new DConnectionMaker();
    UserDao dao = new UserDao(connectionMaker);
    ...
  }
}
  • UserDaoTest는 UserDao와 ConnectionMaker 구현 클래스와의 런타임 오브젝트 의존관계를 설정하는 책임을 담당한다.

1.3.4 원칙과 패턴

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

  • 기능을 확장하는 데 클래스나 모듈은 열려 있어야 하고, 변화에는 영향을 받지 않고 유지할 수 있도록 변경에는 닫혀있어야 한다.

높은 응집도와 낮은 결합도(high coherence and low coupling)

높은 응집도

  • 응집도가 높다는 것은 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것으로 설명할 수 있다.

    낮은 결합도

  • 낮은 결합도는 관계를 유지하는 데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요 없게 만들어주는 것이다.

전략 패턴

  • 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 만드는 디자인 패턴이다.

1.4 제어의 역전(Inverion of Control)

1.4.1 오브젝트 팩토리

팩토리

  • 이 클래스의 역할은 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것이다.
  • 단, 추상 팩토리 패턴과 팩토리 메소드 패턴과는 다르다.
  • 팩토리 역할을 맡을 클래스를 DaoFactory 라고 하자.
public class DaoFactory {
  public UserDao userDao() {
    ConnectionMaker connectionMaker = new DConnectionMaker();
    UserDao userDao = new UserDao(connectionMaker);
    return new UserDao(connectionMaker());
  }
}

public class UserDaoTest {
  public static void main(String[] args) throws SQLException {
    UserDao dao = new DaoFactory().userDao();
  }
}
  • 팩토리는 컴포넌트의 구조와 관계를 정의한 설계도와 같은 역할을 한다.

1.4.2 오브젝트 팩토리의 활용

  • DaoFactory 에 UserDao 가 아닌 다른 DAO의 생성 기능을 넣는다면 아래와 같다.
public class DaoFactory {

  public UserDao userDao() {
    return new UserDao(connectionMaker());
  }

  public AccountDao accountDao() {
    return new AccountDao(connectionMaker());
  }

  public MessageDao messageDao() {
    return new MessageDao(connectionMaker());
  }

  public ConnectionMaker connectionMaker() {
    return new DConnectionMaker();
  }
}

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

  • 제어의 역전이란 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는 것을 말한다.

1.5 스프링의 IoC

1.5.1 오브젝트 팩토리를 이용한 스프링 IoC

애플리케이션 컨텍스트와 설정 정보

  • 스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(Bean)이라고 부른다.
  • 스프링에서는 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(Bean Factory), 혹은 좀 더 확장한 Application Context 라고 부른다.
  • 애플리케이션 컨텍스트는 빈의 생성, 관계 설정 등의 제어 작업을 총괄한다.

DaoFactory 를 사용하는 애플리케이션 컨텍스트

  • 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 스프링이 인식하기 위해서는 @Configuration 이라는 어노테이션을 추가한다.
  • 그리고 오브젝트를 만들어 주는 메소드에는 @Bean 이라는 어노테이션을 붙여준다.
  • ApplicationContext에는 많은 구현체가 있는데, 위와 같은 @Configuration이 붙은 자바 코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.

1.5.2 애플리케이션 컨텍스트의 동작방식

  • 오브젝트 팩토리에 대응되는 것이 스프링의 애플리케이션 컨텍스트다.

  • 애플리케이션 컨텍스트는 DaoFactory 클래스를 설정 정보로 드으록해두고 @Bean 이 붙은 메소드의 이름을 가져와 빈 목록을 만들어둔다.

  • 클라이언트가 getBean() 메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메소드를 호출해서 오브젝트를 생성시킨 후 돌려준다.

애플리케이션 컨텍스트의 장점

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
  2. 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
    • 오브젝트가 만들어지는 방식, 시점과 전략 등을 관리해준다.
  3. 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
    • 타입만으로 빈을 검색하거나 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있다.

1.5.3 스프링 IoC 용어 정리

  • 빈: 스프링 IoC 방식으로 관리하는 오브젝트
  • 빈 팩토리: 스프링의 IoC 를 담당하는 핵심 컨테이너
  • 어플리케이션 컨텍스트: 빈 팩토리를 확장한 IoC 컨테이너
  • 설정정보/설정 메타정보: 스프링의 설정정보란 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타 정보를 말한다.
  • 컨테이너 또는 IoC컨테이너: 애플리케이션 컨텍스트나 빈 팩토리를 IoC컨테이너라고 한다.
  • 스프링 프레임워크: IoC컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 사용한다.

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

오브젝트의 동일성과 동등성

  • 동일성은 == 연산자로, 동등성은 equlas()로 비교한다.
  • 동일하다는 것은 하나의 오브젝트만 존재하고, 두 개의 오브젝트 레퍼런스 변수를 갖고 있다는 것이다.

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

  • 스프링은 기본적으로 다른 설정을 하지 않으면 모든 빈 오브젝트를 싱글톤으로 만든다.

    서버 애플리케이션과 싱글톤

  • 매번 클라이언트 요청마다 각 오브젝트를 새로 생성한다면, 부하가 걸리고 서버가 감당하기 힘들다.
  • 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍이 사용해왔는데, 서블릿이 그 예중 하나이다.
  • 서블릿은 대부분의 멀티 스레드 환경에서 싱글톤으로 동작하며, 사용자 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시 사용한다.

싱글톤의 문제점

  1. private 생성자를 갖고 있기 때문에 상속할 수 없다.
  2. 싱글톤은 테스트 하기 어렵다.
    • 만들어지는 방식이 제한적이어서 목 오브젝트 등으로 제한하기 어렵다.
  3. 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장할 수 없다.
    • 서버에서 클래스 로더를 어떻게 구성하냐에 따라서 하나 이상의 오브젝트가 만들어질 수 있다.
    • 여러 JVM에 분산되어 설치되는 경우에도 각각 독립적으로 오브젝트가 생성된다.
  4. 싱글톤의 사용은 전역 상태를 만들 수 있어서 바람직하지 못하다.
    • 싱글톤의 스태틱 메서드를 이용해 언제든지 싱글톤에 쉽게 접근할 수 있기 때문에 애플리케이션 어디서든지 사용될 수 있고, 자연스럽게 전역 상태로 사용되기 쉽다.

싱글톤 레지스트리

  • 스프링 컨테이너는 싱글톤 레지스트리인데, 스태틱 메서드와 private 메서드를 사용하지 않는 평범한 클래스를 싱글톤으로 활용하게 해준다.

1.6.2 싱글톤과 오브젝트의 상태

  • 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다.
  • 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지 방식으로 만들지 않는다. 왜냐하면, 저장할 공간이 하나뿐이어서 서로 값을 덮어쓰고 자신이 저장하지 않은 값을 읽을 수 있기 때문이다.
  • 그러나 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다.

1.6.3 스프링 빈의 스코프

  • 빈이 생성되고, 존재하고 적용되는 범위를 빈의 스코프라고 한다.
  • 스프링에서 만들어지는 대부분의 빈은 싱글톤 스코프를 갖는다.

1.7 의존관계 주입

1.7.1 제어의 역전과 의존관계 주입

  • 스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불린다.

1.7.2 런타임 의존관계 설정

의존 관계

  • 두 클래스 혹은 모듈이 의존관계에 있다고 말할 때는 항상 방향성을 부여해줘야 한다.
  • A가 B에 의존할 경우, A는 B의 변화에 영향을 받지만, B는 A의 변화에 영향을 받지 않는다.

DI

  • DI 컨테이너는 생성자의 파라미터로 이미 만들어진 오브젝트를 전달한다.
  • 주입이라는 것은 외부에서 내부로 무엇인가를 넘겨줘야 하는 것인데, 자바에서 오브젝트에 무언가를 넣어준다는 개념은 메서드를 실행하면서 파라미터로 오브젝트의 레퍼런스를 전달해주는 방법이다.
  • DI 컨테이너는 자신이 결정한 의존관계를 맺어줄 클래스의 오브젝트를 만들고, 이 생성자의 파라미터로 오브젝트의 레퍼런스를 전달해준다.
  • DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC 개념에 잘 들어 맞는다.

1.7.3 의존 관계 검색과 주입

  • 스프링이 제공하는 IoC 방법에는 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색(dependency lookup)이라고 불리는 것도 있다.
  • 스프링의 IoC 컨테이너는 getBean() 이라는 메서드를 통해 이 기능을 제공한다.
  • 의존관계 검색과 주입의 중요한 차이점 하나는, 의존관계 검색 방식에서는 검색하는 오브젝트는 스프링 빈일 필요가 없다는 것이다.

1.7.5 메서드를 이용한 의존관계 주입

  • 수정자 메서드를 이용한 주입
    • 수정자 메서드는 오브젝트 내부의 애트리뷰트 값을 변경하려는 용도로 주로 사용된다.
    • 수정자 메서드는 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메서드에서 사용하게 하는 DI 방식에서 활용하기 적당하다.
  • 일반 메서드를 이요한 주입
    • 생성자가 수정자 메서드보다 나은 점은 한 번에 여러개의 파라미터를 받을 수 있다는 점이다.
    • 임이의 초기화 메서드를 사용하는 DI는 적절한 개수의 파라미터를 가진 여러 개의 초기화 메서드를 만들 수 있기 때문에 한 번에 모든 필요한 파라미터를 다 받아야 하는 생성자 보다 낫다.

1.8 XML을 이용한 설정

1.8.1 XML 설정

자바 코드 설정 정보 XML 설정정보
빈 설정 파일 @Configuration <beans>
빈의 이름 @Bean methodName() <bean id="methodName"
빈의 클래스 return new BeanClass() class="a.b.c...BeanClass">

XML 의존관계의 주입 정보

  • <property> 태그의 name과 ref는 의미가 다르다.
  • name 애트리뷰트는 DI 에 사용할 수정자 메소드의 프로퍼티 이름이며, ref 애트리뷰트는 주입할 오브젝트를 정의할 빈의 ID이다.
반응형

'도서기록 > 토비의 스프링' 카테고리의 다른 글

5장 서비스 추상화 - 2  (0) 2025.04.09
5장 서비스 추상화 - 1  (0) 2025.04.04
3장 템플릿  (0) 2025.03.22