728x90

지난번에 작성한 UserDao를 다시보자.

    public void deleteAll() throws SQLException {
        Connection connection = dataSource.getConnection();

        PreparedStatement preparedStatement = connection.prepareStatement("delete from users");

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

 

바로 알겠지만 executeUpdate()에서 에러가 발생하면 connection등의 리소스가 닫히지 않고 메서드가 종료된다.

이렇게 반환되지 않고 connection이 계속 열리게 된다면 서버의 운영 중에 큰 문제가 발생한다.(일반적으로는 커넥션 풀을 사용하지만)

 

그렇기 때문에 어떤 상황에서도 connection이 닫힐 수 있도록 try/catch/finally 구문을 사용해야 한다.

 

    public void deleteAll() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement;
        try{
            connection = dataSource.getConnection();

            preparedStatement = connection.prepareStatement("delete from users");

            preparedStatement.executeUpdate();
            preparedStatement.close();
        } finally{
            if(connection != null){
                connection.close();
            }
        }
    }

 

 

이렇게 변경해보았다.

 

만약 connection을 생성하는 도중에 에러가 발생할 수 있고, 그런 connection에서 close()를 호출하면 NPE가 발생하기 때문에 null 체크를 해줘야 한다.

 

JDBC를 통해서 조회를 하는 경우에는 예외처리가 더 어려워진다.

ResultSet이 추가되기 때문이다.

 

그렇기 때문에 ResultSet까지 체크하며 닫아주어야 한다.

    public int getCount() throws SQLException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try{
            connection = dataSource.getConnection();
            preparedStatement = connection.prepareStatement("select count(*) from users");
            resultSet = preparedStatement.executeQuery();
            resultSet.next();
            return resultSet.getInt(1);
        }finally {
            if(resultSet != null){
                try{
                    resultSet.close();
                }catch (SQLException ignored){

                }
            }

            if(preparedStatement != null){
                try{
                    preparedStatement.close();
                }catch (SQLException ignored){

                }
            }

            if(connection != null){
                try {
                    connection.close();
                }
                catch (SQLException ignored){

                }
            }
        }
    }
728x90

개발자는 보통 자신이 작성한 코드에 대하여 테스트를 작성하지만, 다른 사람이 작성하거나 프레임워크에 대해서도 테스트를 작성한다고 한다.

진짜 테스트하려는 목적보다 사용방법에 대해 배우려고 하는 것이다.

 

이렇게 작성한 학습테스트의 장점은 다음이 있다고 한다.

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
  • 프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다.
  • 테스트 작성에 대한 좋은 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다??

그렇기에 몇가지의 예시 테스트로 테스트에 대해 좀 더 알아보자.

public class JUnitTest {

    static JUnitTest testObject;

    @Test
    public void test1(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }

    @Test
    public void test2(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        testObject = this;
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }

    @Test
    public void test3(){
        System.out.println(this);
        System.out.println(testObject);
        Assertions.assertNotEquals(this, testObject);
        testObject = this;
        System.out.println(this);
        System.out.println(testObject);
        System.out.println();
    }
}

 

일단 위는 성공하는 테스트이다.

 

근데 이상하지 않나?

분명 test2, test3에서 testObject를 this로 해주었기에, 둘 중 하나는 testObject가 this로 테스트는 실패해야 한다.

 

성공하는 이유는 각 메서드가 실행될 때마다 새로운 테스트 오브젝트가 생성이 되기에 일치하지 않는 것이다.

실제로 실행하면 다음과 같이 로그가 작성된다.

 

testObject가 바뀌는 것이 아니라, 테스트 클래스 자체가 새로운 오브젝트로 변경된다.

 

이렇게 테스트를 작성하다보면 스프링에 대해 깊게 알 수 있다고 한다...

728x90

우선 테스트 결과를 검증하는 부분을 자동화로 만들어보자.

이 테스트에서는 add()로 전달한 사용자의 정보와 get()으로 조회하는 사용자의 정보가 일치하는지 검증한다.

 

기존에는 단순하게 로그를 확인해 테스트의 결과를 확인했다면, 이것을 코드로 성공과 실패여부를 출력하도록 만들어보자.

 

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

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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


        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        if(!readUser.getName().equals(createdUser.getName()))
        {
            System.out.println("테스트 실패 - name");
        }
        else if(!readUser.getPassword().equals(createdUser.getPassword()))
        {
            System.out.println("테스트 실패 - password");
        }
        else
        {
            System.out.println("조회 테스트 성공");
        }
    }
}

 

이렇게 결과를 비교하여 테스트의 결과를 확인하고, 그에 맞는 결과를 출력한다.

이러면 출력된 로그에 따라 적절한 대응만 하면 된다.

 

굉장히 좋은 테스트이다.

하지만 main() 메서드를 이용한 테스트 작성법으로는 규모가 커지고 테스트의 수가 많아지면, 부담이 될 것이다.

 

그렇기에 JUnit을 사용해본다.

JUnit은 프레임워크로 IoC를 사용해 main() 메서드에 직접 제어권을 작성해 작성할 필요가 없도록 한다.

 

class UserDaoTest {

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
    	//테스트 코드
    }
}

 

이렇게 test 패키지에 test관련 종속성을 추가하고, 해당 메서드에 @Test annotation을 추가한 후 코드를 옮겨적어주면 된다.

그러면 이렇게 초록색으로 결과가 나오게된다.

 

다른 방법으로 검증 할 수는 없을까?

지금 이 방법도 결국 우리가 눈으로 로그를 확인해야 하는 것이다.

JUnit에서는 이런 검증을 할 수 있도록 matcher를 제공한다.

첫번째와 두번째의 파라미터를 비교하여 결과를 만들어주는 기능이다.

 

    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {

        ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

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

        User createdUser = new User();
        createdUser.setId("seungkyu");;
        createdUser.setName("승규");
        createdUser.setPassword("password");

        userDao.add(createdUser);

        System.out.println(createdUser.getId() + "계정을 등록했습니다.");

        User readUser = userDao.get("seungkyu");

        Assertions.assertEquals(createdUser.getId(), readUser.getId());
        Assertions.assertEquals(createdUser.getName(), readUser.getName());
        Assertions.assertEquals(createdUser.getPassword(), readUser.getPassword());
    }

 

이렇게 Assertions에서 제공하는 메서드들로 값을 검증하면 된다.

 

만약 실패한다면

이런 식으로 결과가 나오게 된다.

 

일단 간단하게 테스트를 알아보았고, 다음부터 JUnit에 대해서 더 깊게 공부해보도록 하자

728x90

@Configuration을 사용하는 것과 그냥 팩토리를 사용하는 것과 다른 것이 없어보인다.

 

만약 팩토리를 사용하여 userDao() 메서드를 여러번 호출한다면 동일한 오브젝트가 돌아올까?

당연히 아니고, 매번 새로운 오브젝트가 생성되어 다른 오브젝트가 만들어진다.

 

하지만 애플리케이션 컨텍스트에서 getBean()을 사용해 가져오는 객체는 모두 동일한 오브젝트를 가져오게 된다.

싱글톤으로 저장하고 관리하기 때문이다.

 

왜 스프링에서는 빈을 싱글톤으로 관리할까?

스프링은 대규모의 요청을 처리하기 위해 만들어졌다.

만약 싱글톤을 사용하지 않는다면 요청마다 오브젝트를 생성해야 하고, 생성에도 비용이 드는 것이 아니라 가비지 컬렉션에도 큰 비용이 들게 될 것이다.

그렇기에 하나의 오브젝트만 생성하고, 해당 오브젝트를 공유하는 싱글톤 패턴을 사용하는 것이다.

 

하지만 기존의 싱글톤 패턴은 약간의 문제점이 있었다. (상속 받으면 기본 생성자가 private이다 등등..)

그렇기에 평범한 자바 클래스를 이런 싱글톤으로 활용할 수 있도록 스프링이 기능을 제공한다.

 

하지만 자바는 멀티 스레드 환경이다.

이러한 상황에서 싱글톤을 사용하게 된다면 인스턴스 변수의 사용에 큰 문제가 발생할 수도 있다.

 

기존의 코드를 다음과 같이 바꿔보자.

package seungkyu;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {

    private final ConnectionHelper connectionHelper;
    
    private Connection c;
    private User user;

    public UserDao(ConnectionHelper connectionHelper) {
        this.connectionHelper = connectionHelper;
    }

    public User get(String id) throws ClassNotFoundException, SQLException {

//        Connection connection = connectionHelper.makeConnection();
        this.c = connectionHelper.makeConnection();
        PreparedStatement preparedStatement = c.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        this.user = new User();
        this.user.setId(resultSet.getString("id"));
        this.user.setName(resultSet.getString("name"));
        this.user.setPassword(resultSet.getString("password"));

        resultSet.close();
        preparedStatement.close();
        c.close();

        return user;
    }
}

 

이러면 UserDao의 User를 공유하게 되기에 해당 메서드를 사용하는 중간에 다른 메서드가 끼어든다면, 내용이 바뀔 수도 있다는 문제가 있다.

 

그렇기에 싱글톤을 사용할 때 다른 싱글톤 빈을 저장하는 용도나 final이라면 인스턴스 변수를 사용해도 괜찮지만, 수정이 될 수 있는 경우에는 인스턴스 변수를 사용하지 않는것이 좋다.

 

 

728x90

책을 읽어봤지만, IOC에 대해 바로 이해할 수는 없어서 자료들을 많이 찾아보고 해당 자료대로 작성해보려 한다.

 

기존에는 개발자가 작성한대로 제어의 흐름이 진행되었다.

이것을 프렘워크가 담당하도록 하는 것을 제어의 역전이라고 한다고 한다.

 

이해가 어려우니 코드로 보도록 하자.

 

만약, UserService를 작성하고 UserServiceImpl로 구현을 해서 개발한다면, 이런 방식으로 UserService를 호출하게 될 것이다.

// 개발자가 직접 객체를 생성하는 방식 (IOC 아님)
UserService userService = new UserServiceImpl();

 

이것에 IOC를 적용한다면 다음과 같다.

// IOC 방식 (스프링이 객체를 대신 생성해주고 주입해줌)
@Autowired
private UserService userService;

 

UserService를 사용은 하지만, 어떤 객체로 사용하는지는 모르고 스프링이 제어해준대로 사용하게 된다.

 

이렇게 생성이나 호출 제어는 프레임워크에게 맡기는 방법을 제어의 역전이라고 한다.

 

스프링에서 굉장히 자주 사용하며, 결합도를 낮춰 확장성과 유지보수에 도움을 준다고 한다.

728x90

1.2에서는 상속을 이용하여 다른 연결을 가지는 UserDao를 만들었었다.

 

상속이 아닌 다른 방법으로도 관심사를 분리할 수 있을 것이라고 생각하기에, 아예 연결을 수행하는 독립적인 클래스를 만들어서 분리해보도록 하자.

 

public class SeungkyuConnection {

    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");

        return DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/tobi",
                "root",
                "seungkyu");
    }
}

 

이렇게 연결만을 수행하는 클래스를 만들고, 해당 클래스를 통해 Dao에서 연결을 생성하도록 만들었다.

public class UserDao {

    private SeungkyuConnection seungkyuConnection;

    public UserDao(){
        seungkyuConnection = new SeungkyuConnection();
    }

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

        PreparedStatement preparedStatement = connection.prepareStatement(
                "insert into users(id, name, password) values(?,?,?)"
        );

        preparedStatement.setString(1, user.getId());
        preparedStatement.setString(2, user.getName());
        preparedStatement.setString(3, user.getPassword());

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {

        Connection connection = seungkyuConnection.getConnection();

        PreparedStatement preparedStatement = connection.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        var user = new User();
        user.setId(resultSet.getString("id"));
        user.setName(resultSet.getString("name"));
        user.setPassword(resultSet.getString("password"));

        resultSet.close();
        preparedStatement.close();
        connection.close();

        return user;
    }
}

 

이런 방법에도 2가지 문제점이 있다고 한다.

 

첫째는 이러한 리펙토링도 add와 get등 기존의 연결을 사용하고 있던 코드들을 찾아서 모두 새로운 방식의 연결로 바꿔줘야 한다는 일이다.

현재는 2가지 부분만 변경을 하면 되지만, 프로젝트가 커지고 파일이 분리될수록 해당 코드들을 모두 찾아서 바꾸어주기는 어려울 것이다.

 

둘째는 UserDao에서 데이터베이스의 연결에 대해 알고 있어야 한다는 점이다.

연결에 대한 정보가 SeungkyuConnection에 존재하기 때문에, 현재 UserDao는 해당 정보들에게 종속되어 버린다.

 

이러한 방식을 해결하기 위해, 두 클래스를 느슨하게 연결하도록 만들어주는 인터페이스를 사용한다.

 

인터페이스를 사용하면 해당 메서드의 정보만 제공하고, 구현에 관한 내용은 신경 쓸 필요가 없기 때문에 연결을 느슨하게 만들어준다.

Connection을 인터페이스로 만든다면 UserDao는 자신이 사용할 클래스에 대해 깊게 알 필요가 없어진다는 것이다.

 

 

이렇게 ConnectionHelper 인터페이스를 구현하고, 해당 객체를 UserDao에서 사용하는 방법으로 해결 할 수 있다.

SeungkyuConnection에서 해당 인터페이스를 구현하고, 데이터베이스의 연결에 대한 정보를 가지고 있다.

 

public class SeungkyuConnection implements ConnectionHelper {

    public Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");

        return DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/tobi",
                "root",
                "seungkyu");
    }
}

 

이러면 UserDao에서는 ConnectionHelper의 makeConnection 메서드를 사용하고 있기 때문에, 사용하는 클래스를 변경하더라도 add(), get()의 메서드에서 코드를 수정해줄 필요가 없다.

 

이렇게 만들어도, UserDao의 생성자에서 사용할 객체의 클래스 이름을 사용해 UserDao를 생성해야 한다는 문제가 있다.

 

    private ConnectionHelper connectionHelper;

    public UserDao(){
        connectionHelper = new SeungkyuConnection(); //SeungkyuConnection으로 생성해줘야 함
    }

 

이러면 데이터베이스의 연결 정보를 변경하고 싶을 때마다, UserDao의 코드를 수정해줘야 한다.

 

이렇게 현재 UserDao에서 직접적으로 SeungkyuConnection을 사용하기 때문에 발생한 문제이다.

UserDao에서는 인터페이스만을 사용하여 해당 클래스를 구현해야 한다.

 

런타임 중에 오브젝트 관계를 만들어주기 위해 

    private ConnectionHelper connectionHelper;

    public UserDao(ConnectionHelper connectionHelper) {
        this.connectionHelper = connectionHelper;
    }

 

이렇게 생성자에서 ConnectionHelper를 구현한 객체를 받고, 해당 객체를 사용해 UserDao를 동작하도록 구현하여 UserDao와 SeungkyuConnection관의 의존관계를 제거한다.

 

사용할 때는

UserDao userDao = new UserDao(new SeungkyuConnection());

이렇게 연결정보를 담고있는 ConnectionHelper를 넘겨주어야 한다.

 

이러면 UserDao는 이제 연결과 데이터베이스의 정보에 신경쓰지 않고, ConnectionHelper를 사용해 데이터베이스를 조작할 수 있다.

 

개방 폐쇄 원칙

지금까지는 Dao를 사용해 다양한 방법으로 리펙토링을 해보고 장단점을 비교했다.

이것의 결과로 개방 폐쇄 원칙(OCP)에 대해 설명해보자.

현재 UserDao는 데이터베이스를 연결하는 방법으로는 확장이 가능하다.

동시에 UserDao는 핵심 로직들은 영향을 받지 않고 유지할 수 있다.

이런 것을 개방에는 열려있고, 변경에는 닫혀있는 개방 폐쇄 원칙이라고 한다.

 

높은 응집도와 낮은 결합도

응집도가 높다고 하는 것은 하나의 모듈과 클래스가 하나의 관심사만 집중하고 있다는 뜻이다.

현재 UserDao에서는 데이터베이스를 통해 사용자의 정보를 관리하는 것에 집중하고 있다.

여기에 데이터베이스를 연결하는 코드가 추가된다면, 사용자의 정보라는 관심사에 어긋나게 된다.

유지보수를 위해 하나의 클래스는 관심사를 가지는 기능들로 높은 응집도를 유지하도록 해야한다.

 

결합도는 하나의 오브젝트가 변경이 일어날 때, 관계를 맺고 있는 다른 오브젝트에게 변화를 요구하는 정도이다.

만약 인터페이스를 사용하지 않았다면, SeungkyuConnection을 변경할 때 UserDao도 계속 같이 변경해줘야 했을 것이다.

이것이 SeungkyuConnection과 UserDao간의 결합이 생기기 때문이었으며, 이를 인터페이스를 통한 의존으로 해결해주었다.

 

이렇게 응집도를 높이고, 결합도를 낮추는 방법으로 유지보수를 더 수월하게 할 수 있도록 해야한다.

728x90

스프링을 사용하면서, 스프링을 깊게 공부해보고 싶다는 욕심이 있었다.

백엔드 개발자로 꾸준하게 성장하고 싶었기에, 이번 기회에 토비의 스프링을 사용하여 스프링의 처음부터 다시 보려고 한다.

 

책은 가격이 좀 나갔지만, 도안탄히엔님께서 주셨다.(감사합니다!!)

 

우선 DAO를 만들어보자.

 

DAO는 데이터베이스와 상호작용하며, 데이터를 조회하거나 조작하는 기능을 전담하는 클래스이다.

 

우선 사용자 정보를 저장하는 클래스이다.

public class User {

    private String id;
    private String name;
    private 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;
    }
}

 

이렇게 필드가 private으로 직접 변경이 불가능하고, getter와 setter로 조작해야 하며 기본 생성자가 존재하는 오브젝트를 자바빈이라고 한다.

 

이렇게 만든 사용자의 정보 객체로 데이터베이스를 조작할 수 있도록 DAO 클래스를 만들어보자.

 

 

package seungkyu;

import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");

        var connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/tobi",
                "root",
                "seungkyu");

        PreparedStatement preparedStatement = connection.prepareStatement(
                "insert into users(id, name, password) values(?,?,?)"
        );

        preparedStatement.setString(1, user.getId());
        preparedStatement.setString(2, user.getName());
        preparedStatement.setString(3, user.getPassword());

        preparedStatement.executeUpdate();

        preparedStatement.close();
        connection.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");

        var connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/tobi",
                "root",
                "seungkyu");

        PreparedStatement preparedStatement = connection.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        var user = new User();
        user.setId(resultSet.getString("id"));
        user.setName(resultSet.getString("name"));
        user.setPassword(resultSet.getString("password"));

        resultSet.close();
        preparedStatement.close();
        connection.close();

        return user;
    }
}

 

이렇게 일단 JDBC 커넥션을 항상 열고 닫는 방법으로 DAO를 만들었다.

 

간단하게 메인 메서드에서 테스트를 해보았는데

 

이렇게 정상적으로 저장되는 것을 볼 수 있다.

 

정상적으로 작동은 하지만, 개선할 부분이 많은 코드라고 한다.

다음 챕터에서 수정해보며 알아보도록 하자.

728x90

인프런 김영한 님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

빈의 생명주기

객체의 초기화 작업과 종료 작업이 어떻게 진행되는지 알아둘 필요가 있다.

예제 코드로

package hello.core.lifeCycle;

public class LifeCycleClient {

    private String print;

    public LifeCycleClient(){
        System.out.println("생성자 호출, print = " + print);
        beanStart();
        call("생성자!");
    }

    public void setPrint(String print){
        this.print = print;
    }

    //빈 시작시 호출
    public void beanStart(){
        System.out.println("beanStart: " + print);
    }

    public void call(String message){
        System.out.println("call: " + print + " message = " + message);
    }

    //빈 종료시 호출
    public void beanClose(){
        System.out.println("close: " + print);
    }
}

이런 class를 만들어보자.

우리는 이 class로 빈의 생명주기에 대해 알아볼 것이다.

package hello.core.lifecycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean//(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그리고 테스트코드로 해당 빈을 열고 닫아보자.

실행해 보면

이렇게 print에 값이 들어가 있지 않은 것을 볼 수 있다.

생성자를 통해 생성하는 단계에는 print에 대한 값이 들어가있지 않고, 모두 생성이 된 후에 setter를 통해 값이 들어가기 때문이다.

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 된다.

그렇기에 스프링은 의존관계 주입이 완료되면 스프링 빈에게 초기화 시점을 알려주는 기능을 제공한다. 당연히 스프링 컨테이너가 종료되기 직전에 소멸을 알려주는 기능도 제공한다.

 

스프링 빈의 LifeCycle

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

의 주기를 가진다.

 

물론 생성자에서 로직들을 추가하여 초기화를 할 수도 있지만, 유지보수를 위해 객체를 생성하는 부분과 초기화하는 부분을 분리는 것이 좋다.

 

생명주기 콜백

스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.

InitializingBean, DisposableBean 인터페이스

설정 정보에 초기화 메서드, 종료 메서드를 지정

@PostConstruct, @PreDestroy annotation

 

  • InitializingBean, DisposableBean 인터페이스

InitializingBean, DisposableBean 인터페이스를 implements 하고 해당 인터페이스들에 있는 메서드들을 afterPropertiesSet(), destroy() 메서드들을 구현하는것이다.

package hello.core.lifeCycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class LifeCycleClient implements InitializingBean, DisposableBean {
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }


    public void afterPropertiesSet() throws Exception{
        System.out.println("NetworkClient.init");
        beanStart();
        call("초기화 연결 메시지");
    }

    public void destroy() throws Exception{
        System.out.println("NetworkClient.close");
        beanClose();
    }
}

이렇게 해당 메서드들을 구현하고 테스트를 진행해보자.

이렇게 생성자가 먼저 호출이 되고, 그 후에 초기화 콜백 메서드가 실행이 된다.

빈의 로직들이 실행이 되고, 소멸 전 콜백 실행 해 소멸이 되는 것을 볼 수 있다.

이 방법은 메서드의 이름을 변경할 수 없고 내가 코드를 고칠 수 없는 외부 라이브러리에는 적용이 불가능하여 스프링 초기에 사용하던 방법이고 최근에는 사용하지 않는다고 한다.

 

  • 설정 정보에 초기화 메서드, 종료 메서드를 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")를 입력해 메서드들을 지정할 수 있다.

바로 해보도록 하자, 우선 LifeCycleClient를 좀 수정하고

package hello.core.lifeCycle;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }
    
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

@Bean에 initMethod와 DestroyMethod를 추가한다.

package hello.core.lifeCycle;

import hello.core.lifeCycle.LifeCycleClient;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        LifeCycleClient client = applicationContext.getBean(LifeCycleClient.class);
        applicationContext.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean(initMethod = "init", destroyMethod = "close")
        public LifeCycleClient lifeCycleClient(){
            LifeCycleClient networkClient = new LifeCycleClient();
            networkClient.setPrint("hello!");
            return networkClient;
        }
    }
}

그러면

이렇게 생명주기에 맞게 실행이 되는 것을 볼 수 있다.

@DestroyMethod에 추록기능이 있어 close, shutdown을 찾아가지만 그래도 지정해주도록 하자.

 

  • @PostConstruct, @PreDestroy annotation

가장 많이 사용하는 방법이라고 한다, 하지만 외부 라이브러리에는 적용하지 못하기 때문에 외부 라이브러리에 사용하려면 위의 방법을 사용해야 한다고 한다.

예상했던대로

package hello.core.lifeCycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class LifeCycleClient{
    private String print;

    public LifeCycleClient() {
        System.out.println("생성자 호출, url = " + print);
    }

    public void setPrint(String print) {
        this.print = print;
    }

    //서비스 시작시 호출
    public void beanStart() {
        System.out.println("LifeCycleClient.beanStart, print = " + print);
    }

    public void call(String message) {
        System.out.println("call: " + print + " message = " + message);
    }

    //서비스 종료시 호출
    public void beanClose() {
        System.out.println("LifeCycleClient.disConnect, print = " + print);
    }

    @PostConstruct
    public void init(){
        System.out.println("LifeCycleClient.init");
        beanStart();
        call("init!");
    }

    @PreDestroy
    public void close(){
        System.out.println("LifeCycleClient.close");
        beanClose();
    }
}

그냥 이렇게 @PostConstruct, @PreDestroy annotation을 달아주는 방법이다.

 

'백엔드 > 스프링' 카테고리의 다른 글

스프링 13일차  (0) 2023.03.25
스프링 12일차  (0) 2023.02.15
스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 8일차  (0) 2023.02.07

+ Recent posts