본문 바로가기
BackEnd/Spring

Spring - 오브젝트와 의존관계(관심사의 분리)

by 규난 2023. 3. 19.
728x90

Spring 이란?

스프링은 자라를 기반으로 한 기술입니다.

 

스프링의 철학은 자바 엔터프라이즈 기술의 혼란 속에서 잃어버렸던 객체지향 기술의 진정한 가치를 회복시키고, 그로부터 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것입니다.

 

스프링이 가장 관심을 많이 두는 대상은 오브젝트입니다.

애플리케이션에서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 자세히 살펴보아야 합니다.

 

결국 오브젝트에 대한 관심은 오브젝트의 기술적인 특징과 사용법을 넘어서 오브젝트의 설계로 발전하게 됩니다.

객체지향 설계의 기초와 원칙, 다양한 목적을 위해 재활용 가능한 설계 방법인 디자인 패턴

좀 더 깔끔한 구조가 되도록 지속적으로 개선해나가는 작업인 리팩토링

오브젝트가 기대한 대로 동작하고 있는지를 효과적으로 검증하는 데 쓰이는 단위 테스트와 같은 오브젝트 설계와 구현에 관한 여러 가지 응용 기술과 지식이 요구됩니다.

 

스프링은 유연하고 확작성이 뛰어난 코드를 만들 수 있게 도와주는 객체지향 설계 원칙과 디자인 패턴의 핵심 원리를 담고 있는

IoC/DI를 프레임워크의 근간으로 삼고 있습니다.

 

초난감 UserDao

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

 

JavaBean 규약을 따르는 User Object

package one;

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

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPassword() {
        return password;
    }

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

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

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

여기서 JavaBean 이란 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 위한 디폴트 생성자와 property에 대한 getter, setter를 가진 오브젝트를 말합니다.

 

사용자 정보를 DB에 넣고 관리할 수 있는 UserDao Object

package one;

import java.sql.*;

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/toby?autoReconnect=True", "toby_use", "db1234");

        // 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/toby?autoReconnect=True", "toby_use", "db1234");

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

 

main()을 이용한 DAO 테스트 코드

만들어진 코드의 기능을 검증하고자 할 때 사용할 수 있는 가장 간단한 방법은 오브젝트 스스로 자신을 검증하도록 만들어주는 것입니다.

main 메소드가 실행되면 전체적인 테스트 흐름은 다음과 같습니다.

  • DB 연결을 위한 connection을 가져옵니다.
  • SQL을 담은 Statement or PreparedStatement를 만듭니다.
  • 만들어진 Statement or PreparedStatement 실행합니다.
  • 조회의 경우 SQL 쿼리의 실행 결과를 ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨줍니다. (여기서는 User)
  • 작업 중에 생성된 Connection, Statement, ResultSet 같은 리소스는 작업을 마친 후 반드시 닫아줍니다.
  • JDBC API가 만들어내는 예외는 잡아서 직접 처리하거나, throws를 선언해서 예외가 발생하면 메소드 밖으로 던집니다.
package one;

import java.sql.SQLException;

public class Main {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        UserDao userDao = new UserDao();

        User user = new User();
        user.setId("rbsks");
        user.setName("한규빈");
        user.setPassword("11111");

        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() + " 조회 성공");
    }
}

// 실행 결과
rbsks 등록 성공
한규빈
11111
rbsks 조회 성공

이 코드들은 문제없이 잘 작동하지만 사실 여러 문제점을 가지고 있습니다.

이제부터 문제점들은 하나씩 개선해 나가보도록 하겠습니다.

DAO의 분리

세상에는 변하는 것과 변하지 않는 것이 있습니다. 하지만 객체지향의 세계에서는 모든 것이 변합니다.

여기서 변한다는 것은 변수나 오브젝트 필드의 값이 변한다는 게 아니라 오브젝트의 설계와 이를 구현하는 코드가 변한다는 뜻입니다.

그래서 개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가입니다.

하지만 실무에서는 당장 구현하고 있는 기능도 만들기 바쁜데 어떻게 미래의 변화를 생각하면서 설계와 기능 구현을 할 수 있냐 말할 수 있지만 실력 있는 개발자는 미래를 위해 설계를 하고 개발을 합니다.

 

객체지향의 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문입니다.

 

미래의 변화에 어떻게 효율적으로 대비할 것인가에 해답은 "변화의 폭을 최소한으로 줄이자"이다.

그렇다면 어떻게 변경이 일어날 때 필요한 작업을 최소화하고, 그 변경이 다른 곳에 문제를 일으키지 않게 할 수 있을까요??

이 부분은 분리와 확장을 고려한 설계를 하면 가능합니다.

 

UserDao로 넘어가 UserDao의 관심사항을 보면

  • 첫째는 DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심
  • 둘째는 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 것
  • 셋째는 작업이 끝나면 사용한 리소르를 반납하는 것

이렇게 3가지로 볼 수 있습니다.

 

첫 번째 관심사를 보시면 DB connection을 가져오는 코드는 중복 코드입니다.

지금처럼 add(User user), get(String id) 메소드가 2개밖에 없을 경우 DB를 현재 MySQL에서 오라클로 변경한다거나 사용자 정보를 변경하는 경우 2개의 메소드만 변경하면 되지만 메소드가 많아지고 다른 클래스에서도 DB connection 코드가 흩어져 있다면, 즉 이렇게 공통된 작업이 한 곳에 집중되지 않는 경우가 많을 경우 변경해야 할 범위 엄청 커지게 될 수 있습니다. 

 

우리는 프로그래밍의 기초 개념 중 관심사의 분리(Separation of Concerns)를 통해 관심이 같은 것끼리 모으고 관심이 다른 것은 가능한 따로 떨어져서 서로 영향을 주지 않도록 분리해야 합니다.

 

첫 번째 관심사인 DB connection 코드 분리

package one;

import java.sql.*;

public class UserDao {

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

        // 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 = getConnection();

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

    // 관심사의 분리
    private Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/toby?autoReconnect=True", "toby_use", "db1234");
        return connection;
    }
}

이렇게 DB connection에 대한 관심사를 분리해주면 DB 연결과 관련된 부분에  변경이 일어났을 경우 getConncetion() 메소드만 수정하면 됩니다.

 

방금 한 작업은 UserDao의 기능에는 아무런 변화를 주지 않았습니다.

여러 메소드에 중복돼서 등장하는 특정 관심사항이 담긴 코드(DB connection)를  별도의 메소드로 분리해낸 것 뿐, 이 작업은 기능에는 영향을 주지 않으면서 코드의 구조만 변경하였습니다. 기능이 추가되거나 변경된 것은 없지만 훨씬 깔금하고 미래의 변화에 좀 더 손쉽게 대응할 수 있는 코드가 되었습니다. 이런 작업을 리팩토링(refactoring)이라고 합니다또한 위에서 사용한 getConnection()이라고 하는 공통 기능을 담당하는 메소드로 중복된 코드를 뽑아내는 것을 리팩토링에서는 메소드 추출(extract method)기법이라고 부릅니다.

 

다음 포스트에서 이어서 진행하겠습니다.

 

출처 - 토비의 스프링 3. 1 Vol 1 스프링의 이해와 원리

728x90