[Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (1)

2026. 6. 9. 15:41·spring

스프링은 자바 환경에서 객체지향 프로그래밍을 가장 중요한 가치로 두고 있다. 

따라서 오브젝트에 집중해서 스프링을 바라보도록 하자.

특정한 모델과 기법을 억지로 강요하지는 않지만 오브젝트를 효과적으로 사용할 기준을 마련해주어

기술 설계, 구현에 관한 실용적인 전략과 베스트 프랙티스를 자연스레 적용할 수 있도록 하는 프레임워크가 스프링이다.

 

1.1 초난감 DAO

1.1.1 User

DAO란?

- Data Access Object로 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하는 오브젝트

- 자바빈 규약을 따르는 오브젝트를 이용하면 편리

더보기

자바 빈
비주얼 툴에서 조작 가능한 컴포넌트이지만 이제는 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다.

1. 디폴트 생성자

- 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다.

- 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문

- 리플렉션: 런타임에 클래스 정보를 읽어 객체를 만들고, 메서드나 필드에 접근할 수 있게 해주는 기능

2. 프로퍼티

- 자바빈이 노출하는 이름을 가진 속성

- 프로퍼티는 setter와 getter를 활용해 수정/조회 가능

1.1.2 UserDao

더보기
public class UserDao {

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

        Connection c = DriverManager.getConnection(
            "jdbc:mysql://localhost/springbook",
            "spring",
            "book"
        );

        PreparedStatement ps = c.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();

        ps.close();
        c.close();
    }

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

        Connection c = DriverManager.getConnection(
            "jdbc:mysql://localhost/springbook",
            "spring",
            "book"
        );

        PreparedStatement ps = c.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"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

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

자신을 엔트리포인트로 설정해 직접 실행 가능한 스테틱 main 메소드 내에서 sysout으로 테스트할 수 있지만, 코드가 복잡하다.

앞으로 생각할 포인트는 다음과 같다.

1. 기능은 충실하게 동작하는데 어떠한 문제점이 있을까?

2. 개선했을 때 장점은 무엇인가?

3. 미래에 주는 유익은?

스프링을 공부하는 것은 이러한 문제 제기와 의문에 대해 답을 찾아나가는 과정이라고 필자는 말하고 있다.

좋은 결론을 내릴 수 있도록 객체지향 기술과 자바 개발 선구자들의 방법은 힌트일 뿐,

최종 결론은 개발자가 만들어야 한다.

 

1.2 DAO의 분리

1.2.1 관심사의 분리

세상에서는 변하는 것과 변하지 않는 것이 있지만, 객체지향 세계에서는 모든 것이 변한다.

변수, 오브젝트 필드뿐 아닌 설계, 구현, 사용자 비즈니스 프로세스, 요구사항은 변화하고 발전하며 기반 기술, 운영 환경 또한 변한다.

따라서 객체는 미래에 대비할 수 있어야한다. 그렇다면 어떻게?

실세계를 모델링한다기보다 가상의 추상 세계를 효과적으로 구성하고, 자유롭고 편리하게 변경/발전/확장할 수 있다는 것에 초점을 두어

분리와 확장을 고려한 설계가 이루어져야 한다.

따라서 한 가지 관심이 한 군데에 집중되게 코드를 작성하는 관심사의 분리가 이루어져야한다.

 

관심이 같은 것끼리는 하나의 객체 또는 친한 객체로,

관심이 다른 것은 가능한 따로 떨어져 서로 영향을 주지 않도록.

 

1.2.2 커넥션 만들기의 추출

기존 UserDao 클래스에서 add() 메서드에는 세 가지 관심사항이 존재한다.

1. DB 커넥션 가져오는 방법

2. 사용자 등록을 위해 DB에 보낼 SQL 쿼리를 만들고 실행

3. 작업이 끝나면 오브젝트를 닫고 공유 리소스를 시스템에 돌려줌

-> 중복 코드 메서드 추출을 실행해보자.

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

		Connection c = getConnection();

		PreparedStatement ps = c.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"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}

	private Connection getConnection() throws ClassNotFoundException, SQLException {
		Class.forName("com.mysql.cj.jdbc.Driver");
		Connection c = DriverManager.getConnection("jdbc:mysql://localhost:3306/tobyspring", "toby", "toby");
		return c;
	}

이후에는 DB 종류나 접속 방법이 바뀌었을 때 getConnection()이라는 메서드만 수정하면 된다.

-> 관심이 집중된 코드만 수정

 

1.2.3 DB 커넥션 만들기의 독립

UserDao 소스코드를 외부에서 사용하고 싶지만 컴파일된 클래스 바이너리 파일만 제공하려 한다.

클라이언트들의 사용 DB가 다르기 때문에 DB 커넥션을 독자적으로 적용하고 싶은 상황이다.

 

1. 상속을 통한 확장

더보기
package tobyspring.user.dao;

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

import tobyspring.user.domain.User;

public abstract class AbstractUserDao {
	public void add(User user) throws ClassNotFoundException, SQLException {

		Connection c = getConnection();

		PreparedStatement ps = c.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();

		ps.close();
		c.close();
	}

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

		Connection c = getConnection();

		PreparedStatement ps = c.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"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}

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

클라이언트의 UserDao가 클래스 레벨로 구분이 가능, 계층구조를 통해 관심사가 분리되어 변경에 용이하다.

새로운 DB 연결은 UserDao를 상속하여 getConnection을 구현해주면 된다.

 

슈퍼 클래스에 기본적인 로직 흐름은 두고 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드로 만들어

서브클래스에서 필요에 맞게 구현하도록 할 수 있다.

더보기

상속을 통해 슈퍼 클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다.

변하지 않는 기능은 슈퍼 클래스에 만들어두고 자주 변경되며 확장할 기능은 서브 클래스에서 구현한다.

 

슈퍼 클래스에서는 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의하고, 

이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다.

더보기

상속을 통해 기능을 확장하게 하는 패턴이다.

슈퍼 클래스 코드에서는 서브 클래스에서 구현할 메소드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다.

주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브 클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는

슈퍼 클래스에서 알지 못한다.

 

서브 클래스에서 오브젝트 생성 방법과 클래스를 결정할 수 있도록 미리 정의해둔 메소드를 팩토리 메소드라 하고,

이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 슈퍼 클래스의 기본 코드에서 독립시키는 방법

하지만 상속을 통한 확장은 몇몇 단점이 있다.

1. JAVA는 단일 상속을 채택해 다른 목적으로 상속이 필요하다면

2. 상위 클래스와 하위 클래스의 관계가 생각보다 밀접해진다.

 

1.3 DAO의 확장

모든 오브젝트는 그에 맞는 성격에 따라 변한다. 그 성격은 이유, 시기, 주기가 모두 다르다.

이제는 DB 연결 방식에 따라 서브 클래스의 코드만 변화하지만 상속이라는 방법을 사용한 사실이 불편하게 느껴진다.

 

1.3.1 클래스의 분리

중복 코드였던 메소드를 추출했고, 상/하위 클래스로 분리까지 했다. 이제는 독립적인 클래스를 만들어볼 차례이다.

더보기
package tobyspring.user.dao;

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

import tobyspring.user.domain.User;

public class IndependentUserDao {

	private SimpleConnectionMaker connectionMaker = new SimpleConnectionMaker();

	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = connectionMaker.makeNewConnection();
		PreparedStatement ps = c.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();

		ps.close();
		c.close();
	}

	public User get(String id) throws ClassNotFoundException, SQLException {
		Connection c = connectionMaker.makeNewConnection();

		PreparedStatement ps = c.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"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}
}

이렇게 분리하게 된다면 상속을 통해 DB 커넥션 기능을 확장해서 사용할 수 있었던 것이 불가능해진다.

SimpleConnectionMaker maker = new SimpleConneconectionMaker();
Conncection c = maker.makeNewConnection();

이 두줄에 UserDao가 종속적이게 된다는 뜻이다.

 

1.3.2 인터페이스의 도입

인터페이스라는 중간 다리로 추상적이고 느슨한 연결고리를 만들어줌으로써 해결이 가능하다.

 

추상화란?

공통적인 성격을 뽑아내 따로 분리하는 작업으로, 자바의 추상화 도구는 인터페이스다.

 

인터페이스는 자신이 구현한 클래스에 대한 구체적인 정보는 모두 감춘다. 

결국 오브젝트가 생성되기 위해 클래스 하나를 선택해야하지만,

인터페이스로 추상화한다면 오브젝트를 만들 때 사용할 클래스는 몰라도 상관이 없다.

더보기
public interface ConnectionMaker {

	public Connection makeNewConnection() throws ClassNotFoundException, SQLException;
}

package tobyspring.user.dao;

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

import tobyspring.user.domain.User;

public class IndependentUserDao {

	private ConnectionMaker connectionMaker = new SimpleConnectionMaker();

	public void add(User user) throws ClassNotFoundException, SQLException {
		Connection c = connectionMaker.makeNewConnection();
		PreparedStatement ps = c.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();

		ps.close();
		c.close();
	}

	public User get(String id) throws ClassNotFoundException, SQLException {
		Connection c = connectionMaker.makeNewConnection();

		PreparedStatement ps = c.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"));

		rs.close();
		ps.close();
		c.close();

		return user;
	}
}

하지만 아직도 오브젝트 생성시 new 연산자를 통해 클래스 내에서 주입되고 있다.

 

1.3.3. 관계설정 책임의 분리

private ConnectionMaker connectionMaker = new SimpleConnectionMaker();

UserDao가 아직 구현 클래스를 알아야 사용이 가능하다.

사용할 오브젝트를 자신이 알고 있는 관심사를 분리해야할 필요가 있다.

한 줄로 짧은 코드이지만 매우 충분한 독립적인 관심사이며 이는 클라이언트 측에서 알아야할 관심사이다.

 

따라서 클라이언트 오브젝트가 제3의 관심사항인 UserDao와 ConnectionMaker 구현 클래스 관계를

결정해주는 기능을 분리하기 적합한 곳이다.

 

런타임 시점에서 오브젝트와 오브젝트의 관계를 설정해줘야한다.

즉, 런타임 시에 한 쪽이 다른 오브젝트의 레퍼런스를 갖고 있는 방식으로 만들어진다.

 

직접 생성자를 호출해 만들 수도 있지만 외부에서 만들어준 것을 사용해도 같다!

 

따라서 외부에서 주입된 ConnectionMaker 인터페이스를 구현한 오브젝트와 관계만 맺도록 만든다면,

클래스 사이의 관계가 아닌 오브젝트 사이의 다이나믹한 관계가 만들어진다.

 

인터페이스 타입을 통해 받아 사용한다면 코드에 다른 클래스 이름이 나타나지 않게 되고,

다형성을 충분히 만족시키는 코드가 된다.

 

IndependentUserDao userDao = new IndependentUserDao(new SimpleConnectionMaker());

클라이언트에서 이렇게 UserDao를 사용한다면 런타임 오브젝트 의존 관계 설정 책임이 분리되는 것이다.

이제는 고객이 자신을 위한 DB 접속 클래스를 만들어 UserDao가 사용할 수 있도록 분리되었다.

 

1.3.4 원칙과 패턴

개방 폐쇄 원칙

- 클래스나 모듈은 확장에 열려있어야 하고 변경은 닫혀있어야 한다.

높은 응집도

- 응집도가 높다는 것은 하나의 모듈/클래스가 하나의 책임만 집중

- 변화 시 변하는 부분이 크기 때문에 모듈에서 어떤 부분이 바뀌었을 때 다른 영향이 있을 지 파악할 요소가 줄어든다.

낮은 결합도

- 책임과 관심사가 다른 오브젝트 또는 모듈과는 느슨하게 연결된 형태를 유지한다.

 

전략 패턴

자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고,

이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔 사용할 수 있도록 하는 디자인패턴

 

'spring' 카테고리의 다른 글

[Spring] 토비의 스프링 Vol.1 2장 - 테스트 (2)  (0) 2026.06.18
[Spring] 토비의 스프링 Vol.1 2장 - 테스트 (1)  (0) 2026.06.17
[Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (2)  (0) 2026.06.11
[Spring] 토비의 스프링 강의 섹션 3 - 스프링 도입  (0) 2026.05.04
[Spring] 토비의 스프링 강의 섹션 3 - 스프링 이전  (0) 2026.05.04
'spring' 카테고리의 다른 글
  • [Spring] 토비의 스프링 Vol.1 2장 - 테스트 (1)
  • [Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (2)
  • [Spring] 토비의 스프링 강의 섹션 3 - 스프링 도입
  • [Spring] 토비의 스프링 강의 섹션 3 - 스프링 이전
hhyun
hhyun
  • hhyun
    푯대를 향하여
    hhyun
  • 전체
    오늘
    어제
    • 분류 전체보기 (12) N
      • spring (7) N
      • 개발 일기 (4) N
      • 기술 아티클, 유튜브 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    java
    Spring
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
hhyun
[Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (1)
상단으로

티스토리툴바