Spring/Spring 기초 및 세팅

Spring day 5

seungwon-1 2026. 6. 22. 19:44

Spring day 5

[Spring 입문 5일차] 계층형 구조와 Repository — 데이터는 누가 다루나

들어가며

4일차에서 지금까지 등장한 어노테이션을 한자리에 정리했다. @SpringBootApplication, @RestController, @Service, @Autowired까지. 정리하고 나니 머릿속에 큰 그림은 잡혔는데, 한 가지가 계속 걸렸다.

@Service가 "비즈니스 로직을 담당한다"는 건 알겠다. 그런데 회원 정보를 저장하고 꺼내오는 일, 그러니까 실제 데이터를 만지는 일은 누가 하지? 컨트롤러가 직접 DB에 접근하면 안 되나? 오늘은 그 질문을 따라가 봤다. 답은 계층(Layer)을 나누는 데 있었다.

1. 왜 계층을 나누는가

Spring 프로젝트는 보통 역할을 세 층으로 쪼갠다.

  • Controller — 요청을 받고 응답을 돌려준다. (바깥세상과의 창구)
  • Service — 비즈니스 로직을 처리한다. (실제 "판단"이 일어나는 곳)
  • Repository — 데이터를 저장하고 꺼내온다. (DB와 대화하는 곳)

처음엔 "그냥 한 군데서 다 처리하면 편하지 않나?" 싶었다. 그런데 한 클래스가 요청 처리 + 로직 + DB 접근을 다 떠안으면, 나중에 DB를 바꾸거나 로직을 고칠 때 코드 전체가 흔들린다. 층을 나눠두면 한 층만 고쳐도 나머지는 그대로 둘 수 있다. 이게 핵심이었다.

2. 요청이 흐르는 순서

클라이언트가 회원 가입을 요청했다고 하면, 흐름은 이렇게 흘러간다.

요청 → Controller → Service → Repository → DB
응답 ← Controller ← Service ← Repository ← DB

각 층은 바로 아래층에게만 일을 시킨다. 컨트롤러는 서비스만 알고, 서비스는 리포지토리만 안다. 컨트롤러가 DB를 직접 건드리는 일은 없다. 줄을 잘 세워둔 느낌이다.

3. @Repository

오늘 새로 만난 어노테이션은 @Repository다. 데이터 접근을 담당하는 클래스에 붙인다. @Service, @Controller와 마찬가지로 이것도 결국 @Component의 한 종류라서, 붙여두면 Spring이 빈(Bean)으로 등록하고 관리해 준다. (3일차에 본 컴포넌트 스캔이 여기서 또 쓰인다.)

역할에 맞는 이름표를 붙여두는 셈이다. 기능은 비슷해도 @Repository라고 적어두면, 코드를 읽는 사람도 "아, 여긴 데이터 다루는 층이구나" 하고 바로 안다.

4. 코드로 보기

회원(Member)을 저장하는 아주 단순한 예시다. 강의에서는 DB를 붙이기 전에 일단 메모리에 저장하는 방식으로 먼저 만든다.

먼저 데이터 접근의 약속(인터페이스)을 정의한다.

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
}

그리고 그 약속을 구현한다. 지금은 그냥 Map에 담아둔다.

@Repository
public class MemoryMemberRepository implements MemberRepository {

    private static final Map<Long, Member> store = new HashMap<>();

    @Override
    public Member save(Member member) {
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
}

서비스는 이 리포지토리를 주입받아서 쓴다. 직접 만들지 않는다. (2일차 DI가 여기서 실제로 동작한다.)

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Long join(Member member) {
        memberRepository.save(member);
        return member.getId();
    }
}

여기서 인상 깊었던 건, 서비스가 MemoryMemberRepository라는 구체적인 클래스가 아니라 MemberRepository 인터페이스에 의존한다는 점이다. 나중에 메모리 대신 진짜 DB 구현체로 갈아끼워도 서비스 코드는 한 줄도 안 바뀐다. 층을 나누고 인터페이스로 약속을 잡아두는 이유가 이거였다.

5. 오늘의 정리

  • Spring은 역할을 Controller / Service / Repository 세 층으로 나눈다.
  • 각 층은 바로 아래층에게만 일을 시킨다. 요청은 위에서 아래로, 데이터는 아래에서 위로 흐른다.
  • @Repository는 데이터 접근 층에 붙이는 이름표이자, 빈으로 등록되는 @Component의 한 종류다.
  • 서비스는 구현체가 아니라 인터페이스에 의존한다. 그래서 아래층을 통째로 바꿔도 위층은 멀쩡하다.

어제는 "어노테이션이 무슨 일을 하나"를 봤고, 오늘은 "그 일들이 어느 층에서 일어나나"를 봤다. 점들이 조금씩 선으로 이어지는 중이다. 다음엔 메모리 대신 진짜 DB와 JPA를 붙여볼 차례다.

'Spring > Spring 기초 및 세팅' 카테고리의 다른 글

Spring day 6  (0) 2026.06.23
Spring day 4  (0) 2026.06.21
Spring day 3  (0) 2026.06.20
Spring day 2  (0) 2026.06.19
Spring day 1  (0) 2026.06.17