본문 바로가기
Framework/Spring

[토비의 스프링] 객체 지향에 대한 이해

by 코딩하는 랄로 2023. 11. 20.
728x90

토비의 스프링을 통해 이해한 내용을 요약하고 정리하기 위한 포스팅이다. 이번 글에서는 객체지향에 대한 이해와 그를 바탕으로 객체 지향에서의 의존성 주입이 왜 중요한지에 대해서 간단하게 알아보겠다. 

 

 

 

객체 지향적이지 않은 DAO

객체 지향적이지 않은 DAO(Data Access Object) 코드를 살펴보면서 어떠한 문제가 발생할 수 있는지를 알아보고 해당 문제들을 객체 지향적으로 해결해나가보자.

 

먼저, 사용자 정보를 저장하기 위한 간단한 User 클래스를 만들자.

package com.chapter1.spring.domain;

public class User{
    int id;
    String name;
    String nickName;

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
}

 

 

해당 클래스의 필드에 맞게 DB에도 id, name, nickName 필드를 갖는 user 테이블을 만들어주자. 

CREATE TABLE user (
    id int primary key ,
    name varchar(20) not null ,
    nick_name varchar(10) not null
);

 

이제 해당 해당 DB를 사용해 데이터를 조회하거나 추가하는 기능을 가지는 UserDAO 클래스를 생성해보자. ( 아래의 DB connection 부분에서 [] 안에 들어있는 내용(DB명, user명, 비밀번호) 은 각자의 DB에 맞게 기입 )

package com.chapter1.spring.dao;

import com.chapter1.spring.domain.User;

import java.sql.*;

public class UserDAO {
    // 예외처리는 생략
    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");

        PreparedStatement ps = conn.prepareStatement(
                "insert into user (id, name, nick_name) values (?,?,?)"
        );

        ps.setInt(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getNickName());
        ps.executeUpdate();

        ps.close();
        conn.close();
    }
    // 예외처리는 생략
    public User get(int id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");

        PreparedStatement ps = conn.prepareStatement(
                "select * from user where id = ?"
        );

        ps.setInt(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setName(rs.getString("name"));
        user.setNickName(rs.getString("nick_name"));

        rs.close();
        ps.close();
        conn.close();

        return user;
    }
}

 

위의 UserDAO 클래스를 보면 객체지향을 모르는 사람이 보았을 때는 아무 이상이 없는 코드일 것이다. 실제로도 위의 코드를 테스트 코드를 작성하여 동작 시켜보면 잘 동작하는 것을 볼 수 있다.

package com.chapter1.spring.test;

import com.chapter1.spring.dao.UserDAO;
import com.chapter1.spring.domain.User;

import java.sql.SQLException;

public class UserTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        UserDAO us = new UserDAO();

        User user = new User();
        user.setId(1);
        user.setName("USER1");
        user.setNickName("Spring");

        us.add(user);

        System.out.println(user.getId() + " 등록 성공!!");

        User user2 = us.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getNickName());

        System.out.println(user2.getId() + " 조회 성공!!");
    }
}


/*
1 등록 성공!!
USER1
Spring
1 조회 성공!!
*/

 

하지만, 과연 잘 동작한다해서 UserDAO 클래스는 올바른 코드일까? 단순히 기능을 동작시킨다는 관점에서는 해당 코드는 올바른 코드일 수 있지만, 객체 지향적인 관점으로 보면 해당 코드는 엉망인 코드인 것이다. 

 

객체 지향 세계에서 객체는 끊임없이 변한다. 객체지향적인 코드는 이러한 변화에도 잘 적응하는 유연성을 지녀야 하고 그러한 변화를 쉽게 적용할 수 있는 확장성을 가지고 있는 코드여야 한다. 위의 코드를 수정하는 단계를 거치면서, 해당 코드가 어떻게 객체 지향적인 코드로 변하는지 살펴보자.

 

 

 

단일 책임 원칙 ( 관심사의 분리 )

객체 지향 프로그래밍 원칙을 보면, 단일 책임 원칙이 있다. 해당 원칙은, 객체가 하나의 작업에만 책임을 지도록 하는 것이다. 그리고 이 원칙은 변화에 유연하게 대처할 수 있도록 해주는 원칙이다. 만약, 위의 UserDAO 코드에서 DB가 mysql이 아닌 다른 DB로 바뀐다면 어떻게 되겠는가?

 

UserDAO의 모든 메소드에서 해당 부분을 수정해야 하는 문제가 발생한다. UserDAO의 메소드 add와 get은 자신의 책임과는 상관없는 DB connection에 대해서 책임을 져야 하는 상황이 발생하는 것이다. 

 

이러한 상황을 막기 위해서는 DB connection이라는 하나의 책임을 지는 메소드를 생성하면 된다.

private Connection dbConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");
        return connection;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection conn = dbConnection();

        //...
    }

    public User get(int id) throws ClassNotFoundException, SQLException {
        Connection conn = dbConnection();

        //...
    }
}

 

이렇게 단일 책임을 지는 메소드를 생성하게 되면, DB connection에 대한 책임이 퍼져 있는 것이 아닌 한 곳으로 모이기 때문에 DB를 수정하는 일이 발생하게 되어도, 해당 메소드만을 수정하면 되기 때문에 이전보다는 변화에 유연한 코드로 바뀌었다.

 

하지만, 과연 위의 코드는 객체 지향이 원하는 수준의 유연함을 갖추었을 까? 저 코드에서 더 유연함을 갖추기 위해서는 어떻게 해야 할까? 바로 확장에도 유연함을 지녀야 한다. 확장성을 지니기 위해서는 특정 작업이 확장이 돼도 해당 코드에는 영향을 미치지 않아야 하는 것이다.

 

즉, 위의 코드를 서비스할 때, 클라이언트마다 다른 DB 를 사용한다고 가정을 해보자. 그렇게 되면, 클라이언트마다 dbConnection 메소드를 수정을 해야 하는 상황이 발생한다. 이러한 과정이 싫어 해당 클라이언트에게 수정해서 사용하라고 코드를 넘겨버리자니 코드가 유출이 될 수 있다. 이러한 상황에서 어떻게 코드를 변경해야 할까?

 

 

 

상속을 통한 확장

먼저, 상속을 이용하는 방법이 있다. UserDAO를 abstract 클래스로 구현하여, 확장이 필요하지 않은 코드는 구현 해놓고, 확장이 필요한 메소드(UserDAO에서는 dbConnection)를 구현이 필요한 추상 메소드로 제공하여, 사용자가 구현하도록 하는 것이다.

public abstract class UserDAO {

    abstract public Connection dbConnection() throws ClassNotFoundException, SQLException;

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

    public User get(int id) throws ClassNotFoundException, SQLException {
        //...
    }

}

 

이렇게 getConnection을 구현하도록 해 놓으면, UserDAO 클래스는 변경을 하지 않으면서, 클라이언트가 해당 기능을 구현한 새로운 DB 연결 클래스를 정의하여 사용할 수 있게 할 수 있다. 상속을 통해 확장성을 가지게 된 것이다.

package com.chapter1.spring.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class SUserDAO extends UserDAO{
    public Connection dbConnection() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");
        return connection;
    }
}

 

하지만, 상속을 통한 확장은 단점이 존재한다. 먼저, 자바에서는 클래스당 하나의 상속만을 허용한다. 그렇기 때문에 만약 UserDAO가 다른 기능(예를 들어, add or get)을 위해 상속을 사용하고 있다면 dbConnection만을 위한 상속을 할 수 없게 되는 것이다. 

 

또한 상속으로 인한 결합은 유연하지 못하다. 상속 관계에서 상위 클래스가 변경이 된다면, 아래의 하위 클래스 또한 수정해야 하는 일이 발생하기 때문이다. 이러한 단점 때문에, 객체 지향적인 구조를 설계하기에 상속은 그렇게 바람직한 정답은 될 수 없는 것이다. 그렇다면 어떻게 해야 할까?

 

 

 

인터페이스를 사용한 확장

자바에는 상속이외에도 인터페이스를 통해서도 기능을 확장하는 것이 가능하다. inteface를 바로 사용하기 보다는 먼저 dbConnection을 일반 클래스로 분리하여 보자.

package com.chapter1.spring.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBConnection {
    public Connection dbConnection() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");
        return connection;
    }
}


// UserDAO는 다시 일반 클래스로...
public class UserDAO {

    private DBConnection dbConn;

    public UserDAO() {
        dbConn = new DBConnection();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection conn = dbConn.dbConnection();

        //...
    }

    public User get(int id) throws ClassNotFoundException, SQLException {
        Connection conn = dbConn.dbConnection();
        
        //...
    }

}

 

 

위의 일반 클래스를 사용한 예제를 통해서 어떠한 문제점이 있는지를 살펴볼 필요가 있다. 먼저, 상속의 단점이 사라졌지만 상속을 통한 확장에서 구현한 확장성을 잃어버린 것을 볼 수 있다. UserDAO가 DBConnection 클래스에 종속(의존)되어 다른 DB를 사용할 때마다 DBConnection 클래스를 수정해야만 한다.

 

또한, UserDAO 클래스는 DBConnection의 메소드 이름을 알고 있어야 하고, DBConnection이라는 클래스의 이름도 명확하게 알고 있어야 한다.(DBConnection의 종속되어 있기 때문에) 예를 들어, DBConnection이라는 클래스가 다른 이름을 바뀌거나, 해당 클래스에 메소드가 이름이 변경이 되면, 그에 종속되어 있는 UserDAO 또한 수정을 해주어야 하는 것이다.

 

이는, 객체 지향에서 바라는 코드가 아니다. 해당 코드가 위와 같은 상황에서 유연함을 가지기 위해서는 클래스간의 의존성(종속성)을 느슨하게 해 줄 필요가 있다. 그를 위한 첫번째 단계는 inteface를 사용하는 것이다.

// 인터페이스
public interface DBConnection {
    Connection dbConnection() throws SQLException, ClassNotFoundException;
}

// 그를 상속 받은 SDBConnection
public class SDBConnection implements DBConnection{
    @Override
    public Connection dbConnection() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/[DBNAME]?useSSL=false&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true", "[userName]", "[password]");
        return connection;
    }
}

// 이를 사용하는 UserDao
public class UserDAO {

    private DBConnection dbConn;

    public UserDAO() {
        dbConn = new SDBConnection();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection conn = dbConn.dbConnection();

        //..
    }

    public User get(int id) throws ClassNotFoundException, SQLException {
        Connection conn = dbConn.dbConnection();

        //...
    }

}

 

 

 

인터페이스를 통해 DBConnection의 구현 메소드명을 정해놓았기 때문에, 이를 상속받아 구현한 클래스의 메소드명이 달라질 일이 없고 확장성 또한 가지게 되었다. 하지만, 여전히 SDBConnection에 종속적이기 때문에 다른 DBConnection로 변경되면 UserDAO 클래스를 수정해야 한다. 이를 해결하기 위해서는 어떻게 해야 할까?

 

 

 

의존성 주입

위의 문제를 해결하기 위해 필요한 것이 의존성 주입이다. 객체와의 관계를 느슨하게 하기 위해서, 객체와의 관계 설정에 대한 책임을 해당 객체를 사용하는 사용자에게 떠넘기는 것이다.

// UserDAO
public class UserDAO {

    private DBConnection dbConn;

    public UserDAO(DBConnection dbConn) {
        this.dbConn = dbConn;
    }

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

    public User get(int id) throws ClassNotFoundException, SQLException {
        //...
    }
}

 

사용자에게 해당 객체를 사용(UserDAO 생성)할 때 필요한 객체를 주입(매개변수로 넘겨줌)받아 사용함으로써, 객체의 관계 설정에 대한 책임으로써 독립하여 어떤 객체를 주입받든 신경을 쓰지 않을 수 있게 되는 것이다!! ( 관계를 맺는 객체가 달라져도 UserDAO의 코드는 수정하지 않아도 됨 => 관계로부터 느슨해짐)

 

해당 코드에서 사용자는 test 코드이기 때문에, test 코드에서 의존성을 주입해주면 된다.

public class UserTest {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        DBConnection dbConn = new SDBConnection();
        //의존성 주입
        UserDAO us = new UserDAO(dbConn);

        User user = new User();
        user.setId(1);
        user.setName("USER1");
        user.setNickName("Spring");

        us.add(user);

        System.out.println(user.getId() + " 등록 성공!!");

        User user2 = us.get(user.getId());
        System.out.println(user2.getName());
        System.out.println(user2.getNickName());

        System.out.println(user2.getId() + " 조회 성공!!");
    }
}

 

이제 UserDAO 코드는 확장성을 가지면서도, 변화에 유연한(종속적이지 않은) 객체 지향적인 코드로 변하였다!!( 개방 폐쇄 원칙) 스프링에서도 이러한 객체들을 컨테이너를 통해 관리하면서 필요할 때마다 객체의 의존성을 주입하여 사용하게 함으로써, 보다 쉽게 객체 지향적인 코드를 작성할 수 있게 도와준다.

 

단순히, 스프링 컨테이너가 알아서 객체를 주입해주어 편하다라는 것에서 끝내는 것이 아닌, 왜 스프링 컨테이너라는 외부에서 의존성을 주입해주는가에 대한 의문을 가지고 이에 대한 이해가 바탕이 되어야 제대로 스프링을 다룰 수 있게 된다.

 

다음 글에서는 스프링을 이용하여 위의 코드를 바꾸어보는 과정을 통해 새로운 의문을 갖고 그로 인해 새로운 것들을 배워보자.

 

 

 

 

 

참조 : 이일민, 토비의 스프링 3.1 - 스프링의 이해와 원리, 에이콘, 2012

728x90