ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Study] 토비의 스프링 1주차 스터디 회고: 스프링의 핵심 철학
    Spring 2025. 11. 3. 16:50

    스터디를 시작하며

    혼자서 스프링을 공부하며 프로젝트를 진행해왔지만, 개념을 깊이 이해하기보다는 일단 작동하게 만드는 데 집중했던 것 같다.

    그러다 문득 이런 생각이 들었다.

    스프링은 왜 이런 구조를 택했을까? 왜 스프링에서는 이런 기능들을 제공할까?

     

    이런 근본적인 의문을 해소하기 위해, 동기, 선배들과 함께 토비의 스프링 Vol.1을 교재로 하는 스터디를 시작했다.

     

    스터디는 스프링의 바이블로 불리는 토비의 스프링 Vol.1로 진행되며, 각 주차별로 정해진 범위를 읽고, 발표자가 내용을 정리해 발표한 뒤 각자 읽으며 인상 깊었던 부분이나 궁금했던 점을 함께 토론하는 방식으로 진행된다.
    발표는 매주 돌아가면서 진행한다.

     

    오늘의 목표

    오늘 스터디는 토비의 스프링 Vol.1 1장: 오브젝트와 의존관계.

     

    겉보기엔 단순히 UserDao를 리팩토링하는 내용이지만, 그 안에는 스프링의 핵심 개념 (IoC, DI, Bean, ApplicationContext, DataSource)이 모두 숨어 있었다.

     

     

    초난감 DAO

    가장 처음, 책에는 이렇게 생긴 UserDao가 있었다.

    public class UserDao {
        public void add(User user) throws ClassNotFoundException, SQLException {
            Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/spring", "root", "1234"
            );
            ...
        }
    }

    처음엔 별문제 없어 보였다.

     

    그런데 조금만 생각해보면 문제투성이였다.

    • 매번 DB 연결할 때마다 Connection 생성 코드가 중복
    • DB 연결정보(URL, ID, PW)가 코드에 하드코딩
    • DB 종류가 바뀌면 UserDao 전체를 수정해야 함

    즉, 이 코드는 완전히 DB에 종속된 구조였다. 스프링이 강조하는 유연하고 변경에 강한 객체지향과는 정반대였다.

     


     

     

    ConnectionMaker의 분리: 의존을 끊어보자

    그래서 가장 먼저 처음 리팩토링 한 부분은 DB 연결을 담당하는 부분을 별도의 인터페이스로 분리했다.

    public interface ConnectionMaker {
        Connection makeConnection() throws SQLException, ClassNotFoundException;
    }
    
    public class DConnectionMaker implements ConnectionMaker {
        public Connection makeConnection() {
            return DriverManager.getConnection(...);
        }
    }

     

    그리고 UserDao에서는 더 이상 DB 연결 방법을 알 필요가 없게 만들었다.

    public class UserDao {
        private ConnectionMaker connectionMaker;
    
        public UserDao(ConnectionMaker connectionMaker) {
            this.connectionMaker = connectionMaker;
        }
    }

     

    이걸로 UserDao는 DB 연결 방식과 독립되었다.
    DB가 Oracle이든 MySQL이든 상관없이, ConnectionMaker 구현체만 바꾸면 됐다. 

     


     

     

    그런데..

    현재 구조에서는 

    UserDao는 DB 연결에서 독립됐는데, 지금은 UserDaoTest가 ConnectionMaker를 직접 만들어버린다. 

    ConnectionMaker connectionMaker = new DConnectionMaker();
    UserDao dao = new UserDao(connectionMaker);

     

    그렇다. 이제 의존성이 UserDaoTest로 옮겨간 것뿐이었다.
    결국 테스트 코드가 또 다른 책임(객체 생성)을 떠안고 있었다.

     

     

    "테스트는 UserDao의 기능만 검증해야지, 객체 조립까지 담당하면 너무 많은 역할을 하는 거 아닌가?"

     

    이 문제의식에서 팩토리(Factory)개념이 나온다.

     

     

    팩토리의 등장: 객체 생성 책임을 분리하자

    그래서 UserDaoTest에서 객체 생성 책임을 분리하기 위해 DaoFactory 클래스를 만들었다.

    public class DaoFactory {
    
        public UserDao userDao() {
            return new UserDao(connectionMaker());
        }
    
        public ConnectionMaker connectionMaker() {
            return new DConnectionMaker();
        }
    }

     

    이제 UserDaoTest는 이렇게 단순해졌다 

    UserDao dao = new DaoFactory().userDao();

     

    테스트는 테스트만, 객체 생성은 팩토리가 담당하는 관심사 분리가 완성됐다.

    이 리팩토링은 단순히 코드를 보기 좋게 바꾼 게 아니라, "객체 생성과 사용을 분리한다"는 객체지향의 근본 원칙을 실천한 것이었다.

     

     


     

    IoC (Inversion of Control): 제어의 역전

    이쯤에서 책은 이렇게 말한다.

    "DaoFactory는 스프링의 핵심 원리, IoC(제어의 역전)를 단순한 형태로 구현한 것이다."

     

    기존에는 내 코드가 직접 new를 통해 객체를 생성하고 연결(제어권을 내가 가짐)했다.

    하지만 지금은 DaoFactory가 객체 생성과 연결을 대신 제어한다.

     

      기존 방식 IoC 적용
    제어 주체 내 코드 외부 팩토리
    객체 생성 직접 new 팩토리가 대신
    관계 연결 코드 내부 외부에서 주입

     

    즉, "객체 제어의 주체가 개발자 코드에서 외부로 넘어가는 현상" 이것이 바로 IoC (제어의 역전)이다.

     

     


     

     

    ApplicationContext: 스프링의 팩토리 확장

    책에서는 이후 이렇게 말한다.

    "DaoFactory는 일종의 수동 IoC 컨테이너다. 스프링은 이걸 자동화한 IoC 컨테이너(ApplicationContext)를 제공한다."

     

    DaoFactory가 수동으로 하던 일을 스프링의 ApplicationContext가 대신 처리한다.

    ApplicationContext context =
        new AnnotationConfigApplicationContext(DaoFactory.class);
    
    UserDao dao = context.getBean("userDao", UserDao.class);

    이제 객체 생성, 관계 설정, 관리까지 모두 스프링 컨테이너가 맡는다.

     

    ApplicationContext = IoC 컨테이너, Bean = 그 안에서 관리되는 객체

     

     


     

     

    DI (Dependency Injection): 의존성 주입

    IoC를 더 구체적으로 구현하는 방법이 바로 DI다.

    UserService가 UserDao를 직접 만들면 둘은 강하게 결합되어 변경에 약하다.

    class UserService {
        private UserDao userDao = new UserDao();
    }

     

    그래서 스프링에서는 이렇게 한다.

    @Service
    public class UserService {
        private final UserDao userDao;
    
        @Autowired
        public UserService(UserDao userDao) {
            this.userDao = userDao;
        }
    }

     

    이제 UserDao를 직접 생성하지 않고, 스프링 컨테이너가 UserDao를 만들어서 UserService에 주입(Injection) 해준다.

    IoC가 "제어권을 넘긴다"라면, DI는 "무엇을, 어떻게 주입할지"를 구체화한 것.

     

     

    DataSource: DB 연결도 IoC로 관리하자

    리팩토링의 마지막은 DataSource이다. UserDao가 DB 연결을 이렇게 직접 만들던 시절이 있었다.

    Connection c = DriverManager.getConnection(...);

     

     

    하지만 이 방식은 커넥션 생성 비용이 비싸고, 코드에 DB 정보가 하드코딩되어 있으며, 환경이 바뀌면 직접 수정해야 한다는 단점이 있었다.

    그래서 DB 연결을 관리하는 표준 인터페이스 DataSource 를 도입했다.

    public class UserDao {
        private DataSource dataSource;
    
        public void setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        public void add(User user) throws SQLException {
            Connection c = dataSource.getConnection();
            ...
        }
    }

     

    스프링이 DataSource Bean을 만들어서 UserDao에 주입(DI). 이제 DB 정보가 바뀌어도 application.yml만 수정하면 된다.

    즉, DB 연결마저 스프링이 제어하는 완전한 IoC 구조.

     

     


     

     

    느낀 점

    스터디를 하기 전까진 "스프링은 그냥 자바를 편하게 써주는 프레임워크"라고만 생각했다. 하지만 오늘 깨달았다.

    스프링은 단순한 도구가 아니라, 객체지향 설계를 실천하게 만드는 구조적 장치였다.

    • 객체 생성과 사용의 분리
    • 관심사의 분리
    • 의존성 주입을 통한 유연한 확장성

    이 모든 것이 단지 편의성을 위한 것이 아니라,
    좋은 객체 설계를 강제하는 프레임워크적 철학이었다.

    결국 스프링은, 객체를 "잘 만들고, 잘 연결하고, 잘 관리하게" 만드는 프레임워크다. 

     

     

     

     

Designed by Tistory.