728x90

스프링에서는 스프링이 제어권을 가지고 직접 만들며 관계를 부여하는 오브젝트를 빈이라고 부른다.

 

자바빈과 비슷한 오브젝트 단위이지만, 스프링 빈은 여기에 스프링 컨테이너가 생성하며 관계설정, 사용등을 제어해주는 제어의 역전이 적용된 오브젝트를 말한다.

 

스프링의 빈 팩토리가 사용할 수 있는 설정정보를 만들어보자.

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao(){
        return new UserDao(connectionHelper());
    }

    @Bean
    public ConnectionHelper connectionHelper(){
        return new SeungkyuConnection();
    }
}

 

스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration annotation을 달아야 한다.

 

그리고 이 안의 userDao() 메서드는 UserDao 타입 오브젝트를 생성하고 초기화해서 돌려주는 것이기에 @Bean이 붙는다.

다른 부분도 마찬가지로 @Bean을 설정해준다.

 

이 두가지의 annotation 만으로도 애플리케이션 컨텍스트가 IoC 방식을 제공할 수 있게 된다.

 

 

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.

ApplicationContext를 구현한 클래스 중에서 Annotation을 사용했기에, AnnotationConfigApplicationContext를 사용했다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

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

 

해당 방법으로 Configuration을 ApplicationContext에 등록하고 getBean을 사용해 해당 userDao의 이름의 빈을 UserDao.class로 가져오는 것이다.

 

같은 타입의 메서드가 여러개 지정되어 있다면, 이 이름을 통해서 빈을 가져올 수 있다.

 

이렇게 스프링의 ApplicationContext로 기존의 오브젝트 팩토리를 대체했다.

 

ApplicationContext는 DaoFactory 클래스를 설정정보로 등록해두고 @Bean이 붙은 메서드의 이름을 가져와 빈 목록을 만들어둔다.

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

 

이렇게 IoC 기능으로 ApplicationContext를 사용하면 다음과 같은 장점이 있다.

 

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

프로젝트가 커져가며 Factory들도 점차 많아질텐데, 그러면 클라이언트가 필요한 오브젝트를 가져오기 위해 Factory 클래스들을 찾아야 한다는 문제가 있다. 이를 ApplicationContext에 등록하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.

 

  • ApplicationContext는 종합 IoC 서비스를 제공해준다.

ApplicationContext는 단순하게 오브젝트와 관계설정에만 관여하는 것이 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수 있다고 한다. 이를 통해 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.

 

  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

애플리케이션 컨텍스트의 getBean() 메서드는 빈의 이름을 이용해 빈을 찾아주기에, 특별한 설정이 되어 있는 빈도 찾을 수 있다.

 

이제 스프링의 IoC 용어를 정리하고 마무리해보겠다.

 

  • 빈(Bean)

빈은 스프링 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 빈은 아니다. 그 중에서 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.

 

  • 빈 팩토리(Bean factory)

스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록하고, 생성하고, 조회하고 돌려주고 그 외의 빈을 관리하는 기능을 담당한다. 하지만 보통은 이를 확장한 ApplicationContext를 사용한다.

 

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

빈 팩토리를 확장한 IoC 컨테이너이다. 기본적인 기능은 빈 팩토리와 동일하며, 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 그렇기에 스프링의 기능을 모두 포함하여 이야기 할 때 Application Context라고 말한다.

 

  • 설정정보(Configuration metadata)

빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때도 사용된다.

 

  • 컨테이너

IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너라고 한다. 

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

이번 챕터에서는 구조와 관심사에 관해 이야기 하는 것 같다.

 

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;
    }
}

 

이렇게 만든 코드에서 만약 데이터베이스의 정보를 바꾸려고 한다면, 모든 코드에서 하나하나 코드를 바꿔야 한다는 문제가 생긴다는 것이다.

그렇기 때문에 관심사에 따라 중복되는 코드를 추출해 별도로 분리해야 한다.

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

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

 

이렇게 연결에 관한 부분을 분리했다.

 

만약 데이터베이스의 연결에 관한 정보가 변경된다면, 이 곳에서 한번에 연결에 대한 정보를 변경 할 수 있다.

이렇게 기존의 동작에는 영향을 주지 않으면서 내부 구조를 재구성하는 작업을 리펙토링이라고 한다.

방금 이러한 작업이 리펙토링의 일종이며, 유지보수를 위해 꼭 필요한 작업 중 하나이다.

 

조금 더 리펙토링 해보도록 하자.

만약, 현재 UserDao에서 add와 get의 코드를 공개하지 않고 데이터베이스의 연결을 수정하고 싶다면 어떻게 해야할까?

 

생성자를 통해 데이터베이스의 연결 정보를 받아올 수는 있지만, 일부의 메서드만 수정할 수 있도록 하기 위해서는 객체지향의 특징은 추상화를 이용하면 된다.

 

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection connection = 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 = 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;
    }

    public abstract Connection getConnection() throws ClassNotFoundException ,SQLException;
}

 

이렇게 UserDao를 추상 클래스로, getConnection()을 추상 메서드로 변경한 후

 

public class SeungkyuUserDao extends UserDao{

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

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

 

상속받는 객체에서 해당 추상 메서드를 구현하고

 

UserDao userDao = new SeungkyuUserDao();

 

SeungkyuUserDao를 부모 객체인 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를 만들었다.

 

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

 

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

 

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

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

+ Recent posts