Spring Framework/토비의 스프링

Chapter 01. Object와 의존 관계

계란💕 2023. 3. 19. 16:41

1.1 DAO

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

 


1.1.1 User

<hide/>
public class User {

    String id;
    String name;
    String password;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

 

 

  cf) 자바빈(JavaBean)

  • 자바빈(JavaBean): 원래 비주얼 툴에서 조작 가능한 컴포넌트를 말한다. 
  • 자바의 주력 개발 플랫폼이 웹 기반의 엔터프라이즈 방식으로 바뀌면서 비주얼 컴포넌트로서 자바빈의 인기는 떨어짐
  • 하지만, 자바빈의 몇 가지 코딩 관례는 JSP 빈, EJB같은 표준 기술과 자바 빈 스타일의 오브젝트를 사용하는 오픈 소스 기술을 통해 계속 이어져왔다. 
  • 지금은 "자바빈"이라고 하면 비주얼 컴포넌트라기 보다는 다음 두  관례를 따르는 Object를 가리킨다. ""이라고도 한다. 
    • 1) 디폴트 생성자: 자바빈은 기본 생성자(@NoArgsconstructor)가 필수
    • 2) 프로퍼티: 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. (멤버 변수, 필드)

 


1.1.2 UserDao

<hide/>
public class UserDao {

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


        // add user
        PreparedStatement ps = connection.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();

        // connection close
        ps.close();
        connection.close();
    }

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

        // get user
        PreparedStatement ps = connection.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"));

        // connection close
        rs.close();
        ps.close();
        connection.close();

        return user;
    }
}
  • getConnection() 안에는 로컬에서 설정한 MySQL DataBase 의 id와 비번을 입력해준다. 

 

 

 

JDBC를 이용하는 작업의 순서

  • DB 연결을 위한 Connection 가져온다.
  • SQL을 담은 Statement(또는 PreparedStatement)을 만든다. 
  • Statement 실행
  • SQL 쿼리의 실행 결과를 ResultSet에 담아서 Object에 옮겨준다. 
  • 작업 중 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후, 반드시 닫아준다. (닫아 주지 않으면 문제가 생긴다.)
  • JDBC  API가 만들어내는 예외를 잡아서 직접 처리하거나 throws를  선언해서 메서드 밖으로 던진다. 

 


1.1.3  main() 함수를 통한 DAO 테스트 코드


class.forname( com.mysql.cj.jdbc.driver ) error

  • 오류: ClassNotFoundException 이 발생한다. 
    • 아래의 main 함수를 실행하면 add()의 첫 줄 Class.forname() 부터 오류가 발생한다. 
  • 원인: 해당 프로젝트 폴더에 mysql 라이브러리를 다운 받지 않아서 Class를 찾을 수 없다는 오류가 발생한다. 
  • 해결: 다음 jar 파일을 다운 받아서 프로젝트를 진행하는 폴더 안에 넣어준다. 

mysql-connector-j-8.0.32.jar
2.37MB

  • 다음과 같이 외부 라이브러리에 해당 라이브러리가 추가돼있어야한다. 

 


  Ex)

  • 다음과 같이 MySQL 워크벤치로 데이터베이스와 table을 생성해준다. 

 

  • main 함수
<hide/>
UserDao userDao = new UserDao();
User user = new User();
user.setId("ran");
user.setName("고라니");
user.setPassword("0987");
userDao.add(user);
System.out.println(user.getId() + " 등록 성공");

User getUser = userDao.get(user.getId());
System.out.println(getUser.getName());
System.out.println(getUser.getPassword());
System.out.println(getUser.getId() + " 조회 성공");

 

  Note) 실행 결과

 

 

 

1.2  DAO의 분리


1.2.1 관심사의 분리 (Seperation of Concerns)

  • 객체 지향실세계에 가장 가깝게 모델링할 수 있기 때문에 의미가 있다. 
  • 개발자가 객체를 설계할 때 가장 중요한 건 미래의 변화를 어떻게 대비할 것인가이다. 
  • 미래를 준비하는 건 변화에 어떻게 대비할 것인가에 대한 문제이다. 변화의 폭을 줄이는 것이 가장 좋은 대책이다.
  • 분리와 확장을 고려해서 설계해야 변경 작업을 최소화할 수 있다. 
  • 관심사의 분리: 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이도록 하고 관심이 다른 것은 떨어뜨려서 서로 영향을 주지 않도록 분리하는 것을 말한다. 

 


1.2.2 커넥션 만들기의 추출

 

UserDao의 관심 사항

  1. DB와 연결을 위한 커넥션을 어떻게 가져올 것인가?
    • DB, 드라이버 종류, 커넥션 생성 방법, 등...
  2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행한다. 
  3. 작업이 끝나면 사용한 리소스(Statement, Connection)를 닫아줘서 공유 리소스를 시스템에 돌려준다. 
    • 실무에서 close() 해주지 않으면 큰 문제가 생길 수 있다. 

 

 

UserDao의 문제점

  • 예외 처리가 없다. 
  • DB 연결을 위해 Connection을 가져오는 부분이 get(). add() 메서드에 중복으로 들어가있다. (가장 큰 문제)
    • 하나의 관심사가 방만하게 중복되며 흩어져서 다른 관심 대상과 얽혀 있으면 코드를 변경할 때 큰 문제가 생긴다. 

 

 

UserDao 개선 방법

  • 1) 중복 코드의 메서드 추출: 중복 코드를 분리한다. DB 연결 코드를 독립적인 메서드로 따로 뺀다.
    • 아래 코드 참고
    • 리팩토링에서 메서드 추출 기법이라고 한다. 
  • 2) 변경 사항에 대한 검증: 리팩토링과 테스트

 

  • 1) 메서드 추출: 다음과 같이 getConnection() 을 따로 빼준다. 
    • 만약 DAO 클래스 안에 지금 2개 있는 것과 다르게 메서드가 많은 경우에 수정 사항이 생긴 다면 getConnection() 안에만 수정하면 되기 때문에 훨씬 간단하고 효율적이다. 
<hide/>
private Connection getConnection() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.cj.jdbc.Driver");
    Connection connection = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/tobispring", "root", "4250");
    return connection;
}

 

  •   2) 변경 사항에 대한 검증
    • 기존에 Dao 잘 동작하는 건 테스트해봤는데 코드를 수정한 다음에도 기능에 문제가 없는지는 보장할 수 없다. 
    • 확인하려면 ? main() 한 번 더 실행하면 된다. 
    • 그런데 다시 실행하면 아래와 같은 SQLIntegrityConstraintViolationException 예외가 터진다. 
    • 테이블의  PK로 설정한 id 필드명이 중복되기 때문이다. 

 

 

 

  cf) 리팩토링(Refactoring)

  • 기존의 코드를 외부의 동작 방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업이나 기술을 말한다. 
  • 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고 변화에 효율적으로 대응 가능하다.
  • 코드의 품질 올라가고 유지 보수가 용이하고 견고하면서 유연한 제품을 개발 가능하다. 

 


1.2.3 DB 커넥션 만들기의 독립

  • 앞에서 리팩토링했으나 더욱 품질 좋은 코드를 만드려면 어떻게 해야할까?
  • 만약 여러 고객사의 DB 종류가 다를 수도 있다. 
  • 그리고 고객사에서 독자적으로 만든 방법을 적용해서 DB 커넥션을 가져오고 싶은 경우도 있을 것이다. 
  • 이런 경우라면 고객사에 UserDao의 소스코드를 아예 제공해주고 변경이 필요하면 getConnection()을 사용하라고 할 수도 있다. 
  • 그런데 비밀 기술이 적용된 Dao 클래스라면 ?고객에게 소스 코드를 직접 공개하지 않고 미리 컴파일된 클래스 바이너리 파일만 제공하고 싶은 상황이라면?
  • 이런 경우에 고객 스스로 원하는 DB 커넥션 생성 방식을 적용하면서 UesrDao 를 사용할 수 있을까?

 

 

 

상속을 통한 확장

  • UserDao 에서 메서드의 구현 코드를 제거해서 getConnection() 를 추상 메서드로 만든다. (즉, UserDao 는 추상 클래스)
  • 그러면 get(), add() 메서드에서 getConnection()을 호출하는 코드는 그대로 유지 가능하다. 
  • 그러고 나서 UserDao를 상속해서 각각 NaverUserDao , DaumUserDao 라는 자식 클래스를 만든다. 
  • 그러면 getConnection() 를 원하는 대로 확장한 다음에 UserDao의 기능과 함께 쓸 수 있다. 

 

 

  Ex)

  • UserDao 클래스
<hide/>
public abstract class UserDao {
   private abstract Connection getConnection() throws ClassNotFoundException, SQLException ;
	}
}

 

  • NaverUserDao
<hide/>
public class NaverUserDao extends UserDao {
    private Connection getConnection() throws ClassNotFoundException, SQLException {
        // 고객사 naver의 DB connection 생성 코드 
        return null;
    }
}

 

   Note) 

  • UserDao 코드는 한 줄도 수정할 필요 없이 DB 연결 기능을 새롭게 정의할 클래스를 만들 수 있다. 
  • 새로운 DB 연결을 적용해야한다면 UserDao 상속을 통해 확장하면 된다. 

 

 

 

디자인 패턴(Design Pattern)

  • 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션을 디자인 패턴이라고 한다. 
  • 모든 패턴은 의도와 해결책이 있다.
  • 주로 객체 지향적 설계 원칙을 이용해서 문제를 해결한다. 
    • 여러 디자인 패턴의 설계 구조가 비슷한 이유가 여기에 있다. 
    • 객체지향적인 설계로부터 문제를 해결하기 위해 적용할 수 있는 확장성 추구 방법이 대부분 두 가지로 정리 되기 때문이다.
    • 1. 클래스 상속
    • 2. Object 합성
  • 디자인 패턴은 목적과 의도가 가장 중요
  • 디자인 패턴은 설계 전략이자 편리한 커뮤니케이션 수단이다. 

 

 

 

템플릿 메소드 패턴(Template Method Pattern)

  • 앞서 본 예제와 같이 상위 클래스에 기본적인 로직의 흐름(커넥션 가져오기, sql 실행, 반환, ...)을 만들고 그 기능의 일부를 추상 메서드 또는 오버라이딩이 가능한 protected 메서드 등으로 만든 뒤 자식 클래스에서 필요에 맞게 따라서 구현해서 사용하도록 하는 방법을 말한다. 
  • 상속을 통해 부모 클래스의 기능을 확장할 때 사용하는 가장 대표적 방법이다. 
  • 변하지 않은 기능은 부모 클래스에, 자주 변경되거나 확장할 기능은 자식 클래스에 만들도록 한다. 
  • 부모 클래스에 디폴트 기능을 정의해두고 이를 활용해서 코드의 기본 알고리즘을 담고 있는 템플릿 메서드를 만든다. 
    • 템플릿 메서드 안에는 자식 클래스에서 오버라이드할 여러 메서드를 모아둔다.  
  • 자식 클래스에서 선택적으로 오버라이드할 수 있도록 만든 메서드를 훅(hook) 메서드라고 한다. 
    • 추상 메서드는 자식 클래스에서 반드시 구현해야하지만 훅 메서드는 선택사항이다. 
    • 훅 메서드는 추상 메서드(구현부 없음)와 다르게 구현부 안이 비어 있다. => "{  }"

 

 

팩토리 메소드 패턴(Factory Method Pattern)

  • 자식 클래스인 NaverUserDao의 getConnection() 메서드는 Connection 클래스의 Object를 어떻게 생성할 것인지를 결정하는 방법이다. 
  • 이렇게 자식 클래스에서 구체적인 오브젝트 생성 방법을 결정하는 것을 팩토리 메서드 패턴이라고 한다. 
  • 주로 인터페이스 타입으로 Object를 반환하므로 부모 클래스는 자식 클래스의 해당 메서드의 반환형을 알 수 없다. 
  • cf) 자바에서 종종 Object 생성하는 기능을 가진 메서드를 팩토리 메서드라고도 한다. 팩토리 메서드 패턴과 구분해야한다. 

 

 

  • 템플릿 메소드 패턴과  팩토리 메서드 패턴의 공통점과 차이점
    • 차이점
      • 템플릿 메소드 패턴은 오버라이드한 "메서드의 새로운 구현"에 초점을 맞춘다면 팩토리 메서드 패턴은 메서드를 반환해서 얻은 객체, 즉, "메서드의 반환형(return type)"에 초점을 맞춘다고 생각했다. 
    • 공통점
      • 상속을 통해 기능을 확장한다. 

 

 

 

상속의 문제점

  • 만약 UserDao가 다른 목적을 위해 사용하고 있다면? 
  • 자바는 다중 상속을 허용하지 않는다. 
  • 1) 커넥션 객체를 가져오는 방법을 분리하기 위해 상속 구조로 만들어버리면 후에 다른 목적으로 UserDao 상속을 적용하기 힘들다. 
  • 2) 상속을 통한 상하위 클래스의 관계는 아주 밀접하다.  
    • 자식 클래스는 부모 클래스의 기능을 직접 사용 가능하다. 즉, 부모 클래스 내용이 변경되면 이에 따라 자식 클래스도 수정하는 경우가 생길 수 있다. 

 

 

1.3  DAO의 확장


  • 모든 Object는 변한다. 
  • 관심사에 따라 Object를 분리하는데 각 Object는 독특한 변화의 특징이 있다. 
  • 앞에서 데이터 엑세스 로직을 어떻게 만들 것인지, DB 연결을 어떤 방법으로 할 것인가 두 가지의 관심을 살펴봤는데 이 두 가지 관심은 변화의 성격이 다르다. 
  • 어떤 관심사가 바뀔 때 변화가 일어나는 것이다. 
  • 추상 클래스를 만들어서 상속한 자식 클래스에서 각기 다르게 쓸 수 있도록 만든 이유가 바로 여기 있다. 
    • 변화의 성격이 다른것을 분리해서 서로 영향을 주지 않으면서 각각 필요한 시점에 독립적으로 변경할 수 있도록 하기 위함이다. 
    • 그러나 상속이라는 방법에 불편함이 있다. 

 

 


1.3.1  클래스의 분리 

  • 앞서 살펴본 두 개의 관심사를 본격적으로 독립시키면서 동시에 쉽게 확장하려면 어떻게 해야할까?
  • 상속도 없애고 완전히 독립적인 클래스로 만들어보려한다. 

 

 

  • 다음과 같이 SimpleConnectionMaker라는 클래스는 DB 연결 기능을 가지고 있다. 
<hide/>
public class SimpleConnectionMaker {
     public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
         Class.forName("com.mysql.cj.jdbc.Driver");
         Connection connection = DriverManager.getConnection(
             "jdbc:mysql://localhost:3306/tobispring", "root", "4250");
         return connection;
     }
}

 

 

  • UserDao 
    • 이렇게 바꿀 수 있다. 
<hide/>
public class UserDao {

//    private Connection getConnection() throws ClassNotFoundException, SQLException {
//        Class.forName("com.mysql.cj.jdbc.Driver");
//        Connection connection = simpleConnectionMaker.makeNewConnection();
//
//    }

    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        simpleConnectionMaker = new SimpleConnectionMaker();
    }
    
    public void add(User user) throws ClassNotFoundException, SQLException {
        // db connection
        Connection connection = simpleConnectionMaker.makeNewConnection();
        ;

        // add user
        PreparedStatement ps = connection.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();

        // connection close
        ps.close();
        connection.close();
    }

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

        // get user
        PreparedStatement ps = connection.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"));

        // connection close
        rs.close();
        ps.close();
        connection.close();

        return user;
    }
}

 

  Note) 

  • 기능은 변함 없지만 내부 설계를 변경해서 개선시켰다. 
  • 성격이 다른 코드를 완전히 분리했다. 
  • 그런데 여기서, 네이버와 다음에서 UserDao 클래스만 공급하고 상속을 통해 DB 커넥션 기능을 확장해서 사용하게 했던 게 불가능해졌다. 
    • UserDao 의 코드가 SimpleConnectionMaker 에 종속되어있기 때문에 상속을 했을 때처럼 UserDao 코드의 수정없이 DB 커넥션 생성 기능을 변경할 방법이 없다. 
    • 다른 방식으로 DB 커넥션 을 제공하는 클래스를 사용하기 위해서는 다음 코드를 변경해야한다. 
simpleConnectionMaker = new SimpleConnectionMaker();

 

  • 그럼 이제 UserDao의 소스 코드를 함께 제공하지 않고는 DB 연결 방법을 바꿀 수 없다는 처음 문제로 다시 되돌아온다. 
  • 클래스를 분리한 후에도 상속을 이용할 때 처럼 자유로운 확장이 가능하도록 하려면? 두 가지 문제를 해결해야한다. 
    • 1) SimpleConnectionMaker 안에 메서드가 문제다.
      • makeNewConnection()으로 DB 커넥션을 가져오게 했는데 
      • 만약에 고객사 Daum에서 만든 커넥션 제공 클래스는  openConnection()이라는 이름을 사용했다면 ??
      • add(), get() 메서드의 커넥션을 가져 오는 코드를 다음과 같이 일일이 변경해야한다. (makeNewConnection() => openConnection())
      • 개수가 많아지면 아주 번거로워진다. 
    • 2) DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고 있어야한다는 점
      • UserDao에 SimpleConnectionMaker 라는  클래스 타입의 인스턴스 변수까지 정의해놓고 있으니 고객사 네이버에서 어쩔 수 없이 UserDao 자체를 다시 수정해야한다. 
      • 근본적 원인: UserDao가 바뀔 수 있는 정보인 DB 커넥션을 가져오는 클래스에 대해 너무 많이 알고 있기 때문이다. 
      • 따라서 UserDao 는 DB 커넥션을 가져오는 구체적 방법에 종속되어 버린다. 
      • 고객사가 DB 커넥션 정보를 가져오는 방법을 자유롭게 확장하기 어려워졌다. 

 


1.3.2 인터페이스 도입

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

 

  • ConnectionMaker 인터페이스 
    • makeConnection(): DB 커넥션을 가져온다.
    • 납품할 때 UserDao 클래스와 함께 ConnectionMaker 인터페이스도 전달한다. 
<hide/>
public interface ConnectionMaker {
    public Connection makeConnection() throws ClassNotFoundException, SQLException;
}

 

 

  •  DConnectionMaker 
    • Daum 기업의 개발자라면 다음 클래스를 만들고 자신들의 DB 연결을 통해 가져오도록 메서드를 작성하면 된다.
<hide/>
public class DConnectionMaker implements ConnectionMaker{
    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        // 커넥션 생성 코드
        return null;
    }
}

 

 

  • 인터페이스를 사용하도록 개선한 UserDao
    • 인터페이스만 사용하도록 구성했다. 
    • 따라서, 여러 고객사에서 DB 접속용 클래스를 다시 만든다고 해도 UserDao의 코드를 고칠 일은 없어진다. 
<hide/>
public class UserDao {

    private ConnectionMaker connectionMaker;

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

    public void add(User user) throws ClassNotFoundException, SQLException {
        // db connection
        Connection connection = connectionMaker.makeConnection();
		...
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        // db connection
        Connection connection = connectionMaker.makeConnection();
		...
}

 

  Note) 실행 결과

  • 그러나 아직도 new DConnectionMaker() 라는 이름이 보인다. 
  • 초기에 한 번 어떤 클래스의 오브젝트를 사용할 지 결정하는 생성자 코드는 제거되지 않고 남아있다. 
  • .. 다시 또 원점...
  • 결론: 필요할 때마다 UserDao의 생성자 메서드를 직접 수정하라고 하지 않고는 고객에게 자유로운 DB 커넥션 확장 기능을 가진 UserDao 를 제공할 수가 없다는 뜻이다. 

 


1.3.3 관계 설정 책임의 분리

  • 앞에서 UserDao 와 ConnectionMaker 라는 두 개의 관심을 인터페이스를 이용해서 분리했는데 여전히 UserDao는 여전히 인터페이스뿐만 아니라 구체적 클래스까지 알아야한다. 
  • 왜 이런 문제가 생길까?
  • UserDao에는 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 new DConnectionMaker()라는 코드가 있다. 
  • 이 코드는 기존 Dao의 관심사의 JDBC API와 User 오브젝트를 사용해서 DB에 정보를 넣고 빼는 게 아니고 ConnectionMaker 인터페이스로 대표되는 커넥션을 어떻게 가져올 것인가에 대한 관심사도 아니다. 
  •  new DConnectionMaker() 는 짧고 간단하지만 충분히 독립적인 관심사를 담고 있다
    • UserDao 가  어떤 ConnectionMaker 구현 클래스의 오브젝트를 이용하게 할지를 결정하는 것이다. 
    • Dao와 Dao가 사용할 ConnectionMaker 특정 구현 클래스 사이의 관계를 설정해주는 것에 관한 관심이다. 
    • 이 관심사를 담은 코드를 Dao에서 분리하지 않으면 UserDao는 결코 독립적으로 확장 가능한 클래스가 될 수 없다. 

 

  • 오브젝트 사이의 관계 만들기
    • 일단 만들어진 오브젝트가 있어야한다.
    • 생성자 호출 또는 외부에서 만들어준 것을 가져오는 방법도 있다. 
    • 생성자 호출:  connection = new DConnectionMaker(); 

 


1.3.4 원칙과 패턴

 

개방 폐쇄 원칙(Open Closed Principle)

  • 클래스나 모듈은 확장에 열려 있어야하고 변경에는 닫혀 있어야한다. 
  • ex) 앞에서 살펴본 UserDap는 DB 연결 방법이라는 기능을 확장하는 데는 열려있다. 
  • 맨 앞에서 살펴봤던 초난감 DAO는 개방 폐쇄 원칙을 잘 따르지 못한 설계라고 할 수 있다. 

 

 

높은 응집도와 낮은 결합도

  • 개방 폐쇄 원칙은 높은  응집도낮은 결합도라는 고전적 원리로 설명가능하다. 
  • 높은 응집도
    • 하나의 모듈, 클래스가 하나의 책임이나 관심사에만 집중돼있다는 뜻이다.  클래스 레벨, 패키지, 컴포넌트, 모듈에 이르기까지 그 대상의 크기가 달라도 동일한 원리로 적용된다. 
    • 응집도가 높으면 변화가 일어날 때, 해당 모듈에서 변하는 부분이 크다는 뜻이다. 
  • 낮은 결합도
    • 결합도: 하나의 오브젝트가 변경이 일어날 때 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도를 말한다. 
    • 즉, 하나의 변경이 발생할 때, 다른 모듈이나 객체로 변경에 대한 요구가 전파되지 않는 상태를 말한다. 
    • 느슨하게 연결된 형태를 유지해야한다. 
    • 느슨한 연결은 관계를 유지하는데 꼭 필요한 방법만 간접적인 형태로 제공하고 나머지는 독립적이고 서로 알 필요도 없게 만든다. 
    • 결합도가 낮아지면 변화에 대응하는 속도가 빨라지고 구성이 깔끔해진다. 
    • 높은 응집도보다 더 민감한 원칙이다. 

 

 

전략 패턴(Strategy Pattern)

  • 전략 패턴은 자신의 기능 맥락(context)에서 필요에 따라 변경이 필요한 알고리즘 인터페이스를 통해 통째로 외부로 분리시키고  이를 구현한 구체적인 알고리즘 클래스를 필요에 따라서 바꿔 사용할 수 있게 하는 디자인 패턴이다. 
    • 여기에서 쓰는 알고리즘은 독립적으로 책임으로 분리 가능한 기능을 말한다. 
    • 이를 대체 가능한 전략이라고 보므로 "전략 패턴"이라는 이름이 붙었다. 
  • 디자인 패턴의 꽃이라고 할만큼 자주 쓰인다. 
  • 앞에서 만들었던 UserDao는 전략 패턴의 컨텍스트에 해당한다. UserDao라는 컨텍스트는 자신의 기능을 수행하는데 필요한 기능 중에서 변경 가능한 DB 연결 방식이라는 알고리즘을  ConnectionMaker라는 인터페이스로 정의하고 이를 구현한 클래스(전략)을 바꿔가면서 사용할 수 있도록 분리한다. 

 

 

 

 

1.4 제어의 역전(IoC, Inversion Of Control)


1.4.1 오브젝트 팩토리

  • 팩토리(factory)란?
    • 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 역할을 하는 오브젝트를 보통 팩토리라고 한다. 
    • 오브젝트를 생성하는 쪽과 사용하는 쪽의 역할과 책임을 분리하기 위한 목적으로 사용한다. 
<hide/>
public class DaoFactory {

    public UserDao userDao() {
        ConnectionMaker connectionMaker = new DConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}
  • Userao의 생성 책임을 맡은 팩토리 클래스
    • DaoFactory: 팩토리 역할을 맡는 클래스이다. UserDaoTest에 담겨 있던 UserDao, ConnectionMaker 관련 생성 작업을 DaoFactory로 옮기고 UserDaoTest에서는 DaoFactory에 요청해서 미리 만들어진 UserDao 오브젝트를 가져와서 사용하게 만든다. 
    • 팩토리 메서드는 UserDao타입의 오브젝트를 어떻게 만들고 준비시킬지 결정한다. 

 

<hide/>
public class UserDaoTest {
    public static void main(String[] args) {
        UserDao userDao = new DaoFactory().userDao();
    }
}
  • 팩토리를 사용하도록 수정된 UserDaoTest
    • UserDaoTest는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지 신경쓰지 않고 팩토리로부터 UserDao오브젝트를 받아다가 자신의 관심사인 테스트만 하도록 활용된다. 

 

 

설계도로서의 팩토리

  • UserDao, ConnectionMaker: 애플리케이션의 핵심적인 데이터 로직과 기술 로직을 담당한단. 실질적인 로직을 담당하는 컴포넌트
  • DaoFactory: 이런 애플리케이션의 오브젝트를 구성하고 그 관계를 정의하는 책임을 맡고 있다. 컴포넌트의 구조와 관계를 정의한 설계도 같은 역할이다. 

 


1.4.2 오브젝트 팩토리의 활용

  • DaoFactory에 UesrDao 가 아닌 DAO의 생성 기능을 넣으면 어떻게 될까?
  • AccountDao, MessageDao 등을 만든다고 가정하자. 
  • 이 경우에 UserDao를 생성하는 userDao()를 복사해서 accountDao(), messgeDao() 메서드로 만든다면 새로운 문제가 발생한다. 
  • ConnectionMaker 구현 클래스의 오브젝트를 생성하는 코드가 메서드마다 반복되는 것이다. 
  • 어떤 ConnectionMaker 구현 클래스를 사용할지를 결정하는 기능이 중복돼서 나타나는 것이다. 

 


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

  • 제어의 역전이란 쉽게 말해 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다. 
  • 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선책하지 않는다. 
  • 모든 권한을 다른 대상에게 위임하기 때문이다. 모든 것을 사용하는 쪽에서 제어하는 구조이다. 
  • main() 과 같은 엔트리 포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다. 
  • 일반적인 자바 프로그램은 main()  메서드에서 시작해서 개발자가 미리 정한 순서를 따라서 오브젝트가 생성, 실행된다. 
  • 그런데 서블릿을 개발해서 서버에 배포할 수는 있지만 그 실행을 개발자가 제어할 수는 없다. 
  • 서블릿에 대한 제어 권한을 가지고 있는 컨테이너가 적절한 시점에 오브젝트를 만들고 그 안의 메서드를 호출한다. 
  • 스프링은 IoC를 모든 기능의 기초가 되는 기반 기술로 삼고 있다. 

 

 

1.5 스프링의 IoC


 

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


 

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

  • 빈(bean): 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트이다. 
  • 자바빈 또는 엔터프라이즈 자바빈(EJB)에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말한다. 
  • 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계 설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트이다. 
  • 빈 팩토리(bean factory): 빈의  생성과 관계 설정같은 제어를 담당하는 IoC 오브젝트를 말한다. 
  • 애플리케이션 컨텍스트(application context): 빈 팩토리를 확장한 개념이다. IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 볼 수 있다. (동일하다고 생각하자.  )
    • 빈 팩토리: 빈 생성, 관계 설정하는 IoC 기본 기능에 초점
    • 애플리케이션 컨텍스트: 모든 구성 요소의 제어 작업을 담당하는 IoC 엔진의 의미가 부각 

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

  • @Configuration을 붙이면 스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있다. 
  • 오브젝트 생성을 담당하는 IoC용 메서드에는 @Bean을 붙인다. 
  •  
<hide/>
@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(new DConnectionMaker());
        return userDao;
    }
}

 

  • 두 가지 애너테이션을 붙여서 스프링 프레임워크의 빈 팩토리 또는 애플리케이션 컨텍스트 IoC 방식의 기능을 제공할 때 사용할 설정 정보가 된다. 

 

 

 

  Ex) ApplicationContext  적용하기

  • ApplicationContext 를 구현한 클래스는 여러 가지가 있다. 
  • DaoFactory처럼 @Configuration가 붙은 코드를 설정 정보를 사용하려면 AnnotationConfigApplicationContext를 사용하면 된다. 
  • 생성자의 파라미터로 DaoFactory 클래스를 넣어준다. 
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
UserDao userDao = context.getBean("userDao", UserDao.class);

 

  Note)

  • getBean("빈 이름", 해당 메서드의 반환 형태.class): ApplicationContext 가 관리하는 오브젝트를 요청하는 메서드이다. getBean() 안에 들어가는 파라미터는 ApplicationContext 에 등록된 빈의 이름이다.
    •  앞에서 DaoFactory 안에 선언한 메서드 userDao()는 메서드 이름이 곧 빈 이름이 된다. 
    • 빈 을 호출해서 그 결과인 객체를 가져온다고 생각하면 된다. 
    • cf) getBean() 의 두 번째 파라미터리턴 타입을 주면 지저분한 캐스팅 코드를 사용하지 않아도 된다. 

 

 

 

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


  • 오브젝트 팩토리 이용 방식과 스프링의 애플리케이션 컨텍스트 사용 방식은 뭐가 다를까?

 

 

오브젝트 팩토리 

  • DaoFactory는 UserDao 같은 DAO 오브젝트를 생성하고 DB 생성 오브젝트와 관계 맺어주는 제한적인 역할을한다. 

 

 

애플리케이션 컨텍스트

  • 애플리케이션 컨텍스트 IoC 컨테이너라고도 한다.  ( = 스프링 컨테이너 모두 같은 뜻이다. 또는 빈 팩토리 라고도 한다.  )
    • ApplicationContext는 빈 팩토리가 구현하는 BeanFactory 인터페이스를 상속했기 때문에 애플리케이션 컨텍스트는 일종의 빈 팩토리인 셈이다. 
  • (애플리케이션 컨텍스트를 스프링이라고 부르기도 한다. )
  •  제한적인 역할을 하는 DaoFactory와 다르게 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성 관계 설정을 담당한다. 
  • ApplicationContext 는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없고 별도의 설정 정보를 통해서 생성 정보와 연관 관계 정보를 얻는다. 
  • 애플리케이션 컨텍스트는사용하면 범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해 사용한다. 

 

 

애플리케이션 컨텍스트의 장점 (오브젝트 팩토리 보다 좋은 점)

 

  • 클라이언트는 구체적인 클래스를 알 필요가 없다. 
  • 종합 IoC 서비스를 제공
  • 빈을 검색하는 다양한 방법을 제공: getBean()

 

 

1.5.3 스프링 IoC의 용어 정리


Note

  • 빈(Bean, 빈 오브젝트): 스프링이 IoC 방식으로 관리하는 오브젝트이다. 스프링이 직접 생성, 제어를 담당하는 오브젝트를 빈이라고 한다. 
  • 빈 팩토리(Bean Factory): IoC를 담당하는 핵심 컨테이너를 말한다. 
    • BeanFactory와 같이 붙여쓰면 빈 팩토리가 구현하고 있는 가장 기본적인 인터페이스 이름이 된다. 
    • 이 인터페이스 안에 getBean()이 정의되어 있다. 
  • 애플리케이션 컨텍스트(ApplicationContext): 빈 팩토리확장 IoC 컨테이너이다. 
    • ApplicationContext는 BeanFactory를 상속한다. 
    • 빈 등록하고 관리하는 기능은 빈 팩토리와 동일하지만 애플리케이션 컨텍스트는 스프링이 제공하는 부가 서비스를 추가로 제공한다. 
    • 보통 빈 팩토리보다는 이를 확장한 애플리케이션 컨텍스트를 이용한다. 
  • 설정 정보 / 설정 메타 정보(configuration metadata)
    • 설정 정보(configuration)는 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를  말한다.
  • 컨테이너(Container) 또는 IoC 컨테이너
    • IoC 방식으로 빈을 관리한다는 의미로 애플리케이션 컨텍스트빈 팩토리 컨테이너 또는 IoC 컨테이너 라고 한다. 
    • 컨테이너라는 말은 애플리케이션 컨텍스트보다 추상적인 표현이다. 
  • 스프링 프레임워크
    • IoC컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 때 "스프링 프레임워크"를 사용한다. 
    • 줄여서 스프링이라고도 한다. 

 

 

 

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


 

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

  • 동일성(identity): 두 개의 오브젝트가 완전히 같다.  '=='로 비교한다. 
    • 두 개의 오브젝트가 동일하다면? 사실 하나의 오브젝트만 존재하는 것이다. 두 개의 레퍼런스 변수를 갖고 있을 뿐이다. 
    • 동일하면 항상 동등하다. (반대는 항상 참이라고 볼 수는 없다. )
  • 동등성(equality): 동일한 정보를 담고 있는 오브젝트, 'equals'로 비교한다. 
    • 두 개의 오브젝트가 동일하지 않지만 동등한 경우에는? 두 개의 각기 다른 오브젝트가 메모리 상에 존재하는데 오브젝트의 동등성 기준에 따라 두 오브젝트의 정보가 동등하다고 판단하는 것이다. 

  Ex) 비교 

<hide/>
UserDao userDao1 = factory.userDao();
UserDao userDao2 = factory.userDao();
System.out.println(userDao1);
System.out.println(userDao2);
ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
System.out.println(context.getBean("userDao", UserDao.class));
System.out.println(context.getBean("userDao", UserDao.class));

 

  Note) 실행 결과

  • 스프링은 여러 번에 걸쳐 빈을 요청하더라고 매번 동일한 오브젝트를 돌려준다. 
  • 오브젝트 팩토리는 매번 new에 의해서 새로운 오브젝트를 생성한다. 
com.example.toby.ch01.UserDao@357246de
com.example.toby.ch01.UserDao@28f67ac7
com.example.toby.ch01.UserDao@6f10d5b6
com.example.toby.ch01.UserDao@6f10d5b6

 

 

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


  • 싱글톤 레지스트리(singleton registry)
    • 애플리케이션 컨텍스트는 앞에서 만들었던 오브젝트 팩토리와 비슷한 방식으로 동작하는 IoC 컨테이너이다. 
    • 동시에 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 하다.

 


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

  • 스프링이 싱글톤으로 빈을 만드는 이유는? 
    • 스프링이 주로 적용되는 환경이 자바 엔터프라이즈 기술을 사용하는 서버 환경이기 때문이다. 
  • 스프링으로 PC 등에서 동작하도록 윈도우 프로그램 따위를 만들수도 있으나 실제로는 드물다. 
  • 애초에 스프링은 자바 엔터프라이즈 시스템을 위해 고안된 기술이므로 서버 환경에서 사용될 때 가치가 있다. 
  • 스프링이 처음으로 설계됐던 대규모의 엔터프라이즈 서버 환경은 서버 하나당 최대 초당 수십에서 수백 번씩 브라우저나 여타 시스템으로부터의 요청을 처리하도록 높은 성능이 요구되는 환경이었다. 
  • 그런데 매번 클라이언트에서 요청이 올 때마다 오브젝트를 만들어서 사용한다면 ? 
    • 아무리 가비지 컬렉션의 성능이 좋아졌다고 하더라도 서버가 감당하기 힘들다. 
    • 그래서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 오래전부터 사용했다. 
    • "서블릿"은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 
    • 서블릿은 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들고 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해서 동시에 사용한다. 
  • 이렇게 제한된 (보통 한 개의) 오브젝트를 만들어서 사용하는 게 싱글톤 패턴의 원리이다. 

 


싱글톤 패턴(Singleton Pattern)

  • 어떤 클래스를 애플리케이션 내에서 제한된 인스턴스의 개수, 이름처럼 주로 하나만 존재하게끔 강제하는 패턴을 말한다. 
  • 하나만 만들어지면 애플리케이션 내에서 전역적인 접근이 가능하다. 
  • 가장 자주 활용되지만 가장 비판을 받는 패턴이기도 하다. 
  • 디자인 패턴을 쓴 저자 GoF도 싱글톤 패턴을 매우 조심해서 사용하거나 피하라고 했다. 

싱글톤 패턴의 한계 

  • 자바에서 싱글톤 구현 방법
    • 클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private로 만든다. 
    • 생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 static 필드를 정의한다. 
    • static 팩토리 메서드인 getInstance()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다. 
    • 한번 오브젝트가 싱글톤으로 만들어지고 난 다음에는 getInstance() 를 통해 이미 만들어져  스태틱 필드에 저장해둔 오브젝트를 넘겨준다. 

 

  Ex) UserDao를 싱글톤 패턴을 이용해서 만들면 다음과 같이 될 것이다. 

<hide/>
public class UserDao {
    private static UserDao INSTANCE;

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

    public static synchronized UserDao getInstance(){
        if(INSTANCE == null){
            INSTANCE = new UserDao();
        }
        return INSTANCE;
    }
}
  • 싱글톤을 위한 코드를 추가하고나니까 코드가 지저분해졌다. 
  • 게다가 private로 바뀐 생성자는 외부에서 호출할 수가 없기 때문에 daoFactory에서 UserDao를 생성하며 connectionMaker 오브젝트를 넣어주는 게 이제 불가능해졌다. 
  •  여러 모로 따져보더라도 UserDao에 싱글톤 패턴을 도입하는 건 무리로 보인다. 

 

 


싱글톤 패턴의 구현 방식의 문제점 

  • private 생성자를 가지고 있어서 상속 불가능하다.
    • 오직 클래스 자신 만이 자기 오브젝트를 만들도록 제한한다. 
    • private 생성자를 가진 클래스는 다른 생성자가 없으면 상속이 불가능하다. 
    • 따라서 객체 지향의 장점인 상속과 다형성을 적용할 수 없다. 
  • 싱글톤은 테스트가 힘들다. 또는 불가능하다. 
    • 싱글톤은 만들어지는 방식이 제한적이므로 테스트에서 사용될 때, Mock 오브젝트로 대체가 힘들다. 
  • 서버 환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    • 클래스 로더 구성에 따라 싱글톤 클래스임에도 하나 이상의 오브젝트가 만들어질 수 있다. 
    • 자바 언어를 이용한 싱글 패턴 기법은 서버 환경에서는 싱글톤이 보장된다고 볼 수 없다. 
    • 여러 개의 JVM에 분산돼서 설치되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다. 
  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못한다. 
    • 싱글톤은 사용하는 클라이언트가 정해져있지 않다. 
    • 아무 객체나 자유롭게 접근하고 수정하고 고유할 수 있는 전역 상태르 ㄹ갖는 것은 객체 지향형 프로그래밍에서는 권장되지 않는 프로그래밍 모델이다. 

 


싱글톤 레지스트리(Singletone registry)

  • 스프링은 서버 환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것은 적극 지지한다. 
  • 그러나 자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 
  • 이를 바로 "싱글톤 레지스트리"이다. 
  • 스프링 컨테이너는 싱글톤을 생성, 관리, 공급하는 싱글톤 관리 컨테이너 이기도 한다. 
  • 싱글톤 레지스트리의 장점 
    • static 메서드와 private 생성자를 사용해야하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다. 
    • 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용해서 생성과 관계 설정 사용 등에 대한 제어권을 컨테이너에게 넘기면 손쉽게 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 
    • 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다. 

 

 

 

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


  • 싱글톤은 멀티스레드의 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있어야한다. 
  • 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태 정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야한다. 
  • 다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 위험하다. 
    • 저장할 공간이 하나뿐이라서 서로 값을 덮어쓰고 자신이 저장하지 않은 값을 읽어올 수 있기 때문이다. 
  • 따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태 유지(stateful) 방식으로 만들지 않는다. 
    • 이를 지키지 않으면 여러 사용자들이 동시 접속하는 경우에 데이터가 엉망이 되어 버리는 심각한 문제가 발생할 수 있다. 

싱글톤 패턴(Singleton Pattern)

 

 

  Ex) 인스턴스 변수를 사용하도록 수정한 UserDao

<hide/>
public class UserDao {

    private ConnectionMaker connectionMaker;

    private Connection c;

    private User user;

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

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

        // get user
        PreparedStatement ps = c.prepareStatement(
            " SELECT * " + " FROM users WHERE id = ?;");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        this.user = new User();
        this.user.setId(rs.getString("id"));
        this.user.setName(rs.getString("name"));
        this.user.setPassword(rs.getString("password"));
        rs.close();
        ps.close();
        c.close();
        return this.user;
    }

}
  • ConnectionMaker: 읽기 전용 변수
  • 매번 새로운 값으로 바뀌는 정보를 담은 인스턴스 변수 
  • 기존에는 로컬 변수로 선언하고 사용했던 connection과 User를 클래스의 인스턴스 필드로 선언했다는 것이다. 
  • 따라서 싱글톤으로 만들어져서 멀티스레드 환경에서 사용하면 위에 설명한 대로 심각한 문제가 발생한다. 

 

 

 

1.6.3 스프링 빈의 스코프


  • 스프링이 관리하는 오브젝트, 즉 빈이 생성되고 존재하고 적용되는 범위를 알아보자. 
  • 스프링에서는 이를 빈의 스코프(scope)라고 한다. 
  • 스프링 빈의 기본 스코프는 싱글톤이다. 
  • 싱글톤 스코프는 컨테이너 안에 하나의 오브젝트만 만들어서 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지된다. 
  • 스프링에서 만들어지는 대부분의 빈은 싱글톤 스코프를 갖는다. 
  • 싱글톤 이외의 스코프: 프로토파입(prototype) 스코프, 요청(request) 스코프, 세션(session) 스코프 
  • 프로토 타입 스코프는 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 
  • 요청(request) 스코프: 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 스코프 
  • 세션(session) 스코프: 웹의 세션과 스코프과 유사한 스코프

 

 

 

1.7 의존 관계 주입(DI)


 

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


  • IoC는 소프트웨어에서 자주 발견할 수 있는 일반적인 개념이다. 
  • 보통 "스프링"이라고 하면 컨테이너의 뜻인지 아니며 ㄴ프레임워크 의미인지, 파악이 힘들다. 
  • 그래서 스프링이 제공하는 IoC 방식의 핵심을 집어주는 "의존 관계 주입(DI)"라는 의도가 명확한 이름을 사용하기 시작했다. 

 

 

의존관계 주입, 의존성 주입, 의존 오브젝트 주입 (Dependency Injection)

  • 의존성 주입이라는 말이 가장 많이 사용된다. 
  • DI는 오브젝트 레퍼런스를 외부로부터 제공 받고 이를 통해 여타 오브젝트와 다이나믹하게 의존관계가 만들어지는 것이 핵심이다. 
  • "의존 관계 설정"이라는 용어를 쓰면 의도를 강하게 드러낼 수 있다. 

 

 

1.7.2 런타임 의존 관계 설정


의존 관계

  • 두 개의 클래스 또는 모듈이 의존 관계가 있는 경우, 방향성을 부여해줘야한다. 
  • UML 에서는 A => B라고 나타내면  A가 B에 의존하고 있다는 걸 나타낸다. 
  • 의존이라는 것은 B의 기능 추가 / 변경 / 형식 변경이 일어날 경우에 A에도 그 영향이 미친다는 뜻이다. 

 


UserDao의 의존관계

  • 기존에 작업했던 구조를 보면 UserDao가 ConnectionMaker 인터페이스에 의존하고 있다. 
  • 따라서 ConnectionMaker가 변하면 UserDao가 그 영향을 직접 받게 된다. 
  • 인터페이스를 통해 의존관계를 제한하면 그만큼 변경에서 자유로워진다. 
  • 모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존 관계 외에도 런타임 시에 오브젝틍 사이에서 만들어지는 의존관계도 있다.  => "런타임 의존 관계, 오브젝트 의존 관계"
    • 의존 관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다. 

 

 

의존관계 주입에 대한 세 가지 조건

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야한다. 
  • 런타임 시점의 의존관는 컨테이너나 팩토리 같은 제3 의 존재가 결정한다.
  • 의존관계를 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공해줌으로써 만들어진다. 

 


UserDao의 의존 관계 주입

 

  • 관계 설정 책임 분리 전의 생성자
<hide/>
public class UserDao {
    connectionMaker = new DConnectionMaker();
}

 

  • 위 코드에 따르면 UserDao 클래스 설계 시점에 이미 DConnectionMaker 라는 구체적인 클래스의 존재를 알고있다. 
  • 이 코드의 문제점은 런타임 시의 의존 관계가 코드 속에 다 미리 결정되어 있다는 점이다. 
  • 그래서 IoC 방식을 써서 UserDao로부터 런타임 의존 관계를 드러내는 코드를 제거하고 제3 의 존재에 런타임 의존관계 결정 권한을 위임한다. 
    • 최종적으로 만들어진 것이 DaoFactory이다. 
    • DaoFactory는 앞서 살펴본 세 가지 조건을 모두 만족한다. 
    • DaoFactory는 DI 컨테이너이다. 
  • DI 컨테이너는 UserDao를 만드는 시점에 생성자의 파라미터로 만들어진 DConnectionMaker의 오브젝트를 전달한다. 
  • 생성자를 이용하면 파라미터를 가장 손쉽게 전달할 수 있다. 

 

 

  • 아래 코드와 같이 생성자를이용해서 전달받은 런타임 의존관계를 갖는 오브젝트는 인스턴스 변수에 저장해둔다. 
  • 두 개의 오브젝트 사이에 런타임 의존관계가 만들어졌다. 
  • 이와 같이 DI 컨테이너에 의해 런타임 시에 의존 오브젝트를 사용할 수 있도록 그 레퍼런스를 전달받는 과정이 마치 메서드(생성자)를 통해 DI 컨테이너가  UserDao에게 주입해주는 것과 같다고 해서 이를 "의존 관계 주입"이라고 부른다. 
  • DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC(제어의 역전) 개념에 들어 맞는다. 
  • 그래서 스프링을 IoC 컨테이너, DI 컨테이너, DI 프레임워크 라고 부르는 것이다. 
<hide/>
public class UserDao {
    private ConnectionMaker connectionMaker;
    
    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

 

 

 

1.7.3  의존관계 검색(Dependency Lookup, DL)과 주입


  • 스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 게 아니다. 
  • 의존관계 검색(dependency lookup)도 존재한다. 
  • 의존관계 검색은 자신이 필요로 하는 의존 오브젝트능동적으로 찾는다. 
  • 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 맡기지만 이를 가져오거나 메서드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다. 

 

 

 

  Ex) 의존 관계 검색

 

  • DaoFactory를 이용하는 생성자
    • 아래와 같이 만들어도 UserDao는 자신히 어떤 ConnectionMaker 오브젝트를 사용할지 미리 알지 못한다. 
    • 여전히 ConnectionMaker  인터페이스에만 의존한다. 
    • 런타임 시에 factory가 만들어서 돌려주는 오브젝트와 런타임 의존관계를 맺는다. 
    • IoC 개념을 잘 따르고 있다.
    • 하지만 외부로부터 주입이 아닌 컨테이너인 factory에게 요청하는 방식이다. 
    • DaoFactory 경우에는 미리 준비된 메서드를 호출하니까 단순 요청
<hide/>
public UserDao() {
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

 

 

  • 의존 관계 검색을 이용하는 UserDao 생성자
    • DaoFactory 경우에는 미리 준비된 메서드를 호출하니까 단순 요청으로 보이지만 애플리케이션 컨텍스트의 경우는 이미 정해 놓은 이름을 전달해서 해당 빈을 찾게 된다. => "검색"
    • 애플리케이션 컨텍스트는 getBean()이라는 메서드를 이용해서 의존 관계를 검색할 수 있다.
    • 의존 관계 검색은 의존 관계 주입의 장점을 모두 가지고 있다. 코드가 더 깔끔하다. 
    • 그러나 의존 관계 검색은 코드 안에 오브젝트 팩토리 클래그나 스프링 API가 나타난다. 컴포넌트가 컨테이너처럼 성격이 다른 오브젝트에 의존하므로 바람직하지 않다. 
    • 따라서 일반적으로는 의존 관계 검색 보다는 의존 관계 주입을 사용하는 게 낫다. 
<hide/>
public UserDao() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

 

 

  • 앞에서 의존 관계 검색 다는 의존 관계 주입을 사용하는 게 낫다는 내용을 살펴봤는데 만약 의존 관계 검색을 꼭 사용해야만 하는 경우라면?
    •  애플리케이션 기동 시점에서 적어도 한 번은 의존 관계 검색방식을 사용해서 오브젝트를 가져와야한다.  static 메서드의 main() 같은 기동 메서드는 없으나 사용자가 요청을 받을 때마다 main() 과 비숫한 역할을 하는 서블릿에서 스프링 컨테이너에 담긴 오브젝트를 사용하려면 한 번은 " 의존 관계 검색" 방식을 사용해서 가져와야한다. 
    • 서블릿은 스프링이 미리 만들어서 제공하므로 직접 구현할 필요 없다.
  • 의존 관계 검색 / 의존 관계 주입 둘의 중요한 차이점은?
    • 의존관계검색에서  UserDao에서 getBean()을 사용한 의존관계 검색을 적용한다고 가정하자. 그럼 오브젝트는 자신이 스프링의 빈일 필요가 없다. 직접  new UserDao()해서 만들어도 상관없다. ConnectionMaker만 스프링의 빈이면 된다. 
    • 의존관계주입에서는? UserDao와 ConnectionMaker 사이에 DI를 적용하려면 반드시 둘다 빈 오브젝트여야한다. UserDao에  ConnectionMaker 오브젝트를 주입하려면 UserDao에 대한 생성초기화 권한을 갖고 있어야하고  그러려면 UserDao는이어야하기 때문이다. 
    • DI를 원하는 오브젝트는 먼저 자신이 빈이 되어야한다.

 

 

1.7.4 의존관계 주입의 응용


기능 구현의 교환

  • DI의 장점은?
    • DI(런타임 시에 사용 의존관계를 맺을 오브젝트를 주입)
    • 개발 환경과 운영 환경에서의 설정 정보에 따른 DaoFactory만 다르게 두면 나머지는 변경 사항 없이 개발 시, 운영 시에 각각 다른 런타임 오브젝트에 의존 관계를 갖게 해줄 수 있다.

 


부가 기능 추가

  • DAO가 DB를 얼마나 많이 연결해서 사용하는지 파악하고 싶다면?
  • 연결 횟수를 직접 세는 것은 아주 비효율적인 방법이다. 
  • DI 컨테이너를 이용하면 DAO와 DB 커넥션을 만드는 오브젝트  사이에 연결횟수를 카운팅하는 오브젝트를 추가해서매우 간단하게 가능하다.

 

 

 

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


생성자가 아닌 일반 메서드를 이용해서 의존 오브젝트와의 관계를 주입하는 방법

  • 수정자(setter) 메서드를 이용한 주입: setter는 파라미터를 전달된 값을 내부의 인스턴스 변수에 저장한다. 외부로부터 제공받은 오브젝트 레퍼런스를 저장했다가 내부의 메소드에서 사용하게 하는 DI 방식에서 활용하기에 적당하다. 
  • 일반 메서드를 이용한 주입: 수정자는 하나의 파라미터만 가질 수 있는데 이러한 제약이 싫은 경우, 여러 개의 파라미터를 갖는 일반 메소드를 DI용으로 사용할 수 있다. 
  • 스프링은 수정자 메서드를 가장 많이 사용해왔다. 자바 코드 대신 XML을 사용하는 경우에는 자바빈 규약을 따르는 수정자 메소드가 가장 사용하기 편리하다. 

 

 

 

1.8 XML을 이용한 설정


  • 스프링은 DaoFactory와 같은 자바 클래스외에도 다양한 방법을 통해서 DI 의존 관계 설정 정보를 만들 수 있다. 
  • XML이 가장 대표적인 방법이다. XML은 단순한 텍스트 파일이므로 다루기 쉽다. 

 

 

 

1.8.1 XML 설정


  • 애플리케이션 컨텍스트는 XML에 담긴 DI 정보를 활용할 수 있다. 
  • DI가 담긴 XML 파일은 <beans>를 루트 엘리먼트로 사용한다. 
  • XML 설정은 @Configuration과 @Bean 이 붙은 자바 클래스로 만든 설정과 내용이 동일하다. 
    • @Configuration는 <beans> ,  @Bean은 <bean>라고 대응해서 생각하면 쉽다. 
  • 하나의 @Bean 메소드를 통해 얻을 수 있는 빈의 DI 정보는 다음과 같다.  
    • 빈의 이름: @Bean 메소드 이름이 빈의 이름이다. getBean()에서 사용된다. 
    • 빈의 클래스: 빈 오브젝트를 어떤 클래스를 이용해서 만들지 정의한다. 
    • 빈의 의존 오브젝트: 빈의 의존 오브젝트를 넣어준다. 의존 오브젝트도 하나의 빈이므로 이름이 있을 것이고 그 이름에 해당하는 메소드를 호출해서 의존 오브젝트를 가져온다. 의존 오브젝트는 하나 이상일 수도 있다. 
  • XML에서 <bean>을 사용해도 세 가지 정보를 정의할 수 있다.
  • 더 이상 의존하고 있는 오브젝트가 없는 경우에는 세 번째 의존 오브젝트 정보를 생략 가능하다.
  • XML은 자바 코드처럼 유연하게 정의될 수 있는 것이 아니므로 핵심 요소를 짚어서 그에 해당하는 태그와 애트리뷰트가 무엇인지 알아야한다.

 


connectionMaker() 전환

  • content

 

 

 


title

  • content

 

 


title

  • content

 

 


title

  • content

 

 


title

  • content

찾아보기

  • content

 

 

 

참고) 내용이 잘못됐거나 다르게 생각하시는 부분은 댓글 부탁드립니다❣

출처 - 「토비의 스프링 3.1 Vol 1 - 이일민」