본문 바로가기

SPRING/Spring

[스프링| 스프링 입문 | 코드로 배우는 스프링] 비즈니스 요구사항 정리

Service 계층에서 구현하는 메서드들은 Repository 계층의 메서드와는 다른 역할과 책임을 가지고 있습니다. 일반적으로, Repository 계층은 데이터의 저장과 조회와 같은 데이터 접근을 직접적으로 다루는 반면, Service 계층은 비즈니스 로직의 실행을 책임집니다. 이를 통해 애플리케이션의 비즈니스 요구사항을 충족시키고, 데이터를 가공하거나 복잡한 연산을 수행합니다.

Service 계층의 주요 역할은 다음과 같습니다:

  1. 비즈니스 로직 구현: 데이터가 어떻게 처리되어야 하는지, 어떤 순서로 비즈니스 규칙을 적용해야 하는지를 정의하고 구현합니다. 예를 들어, 사용자 등록 시 유효성 검사, 중복 검사, 권한 부여 등의 추가 로직을 구현할 수 있습니다.
  2. 트랜잭션 관리: 여러 데이터 변경이 한 작업으로 묶이어야 할 경우, Service 계층에서 이러한 트랜잭션을 관리합니다. 예를 들어, 회원 정보를 업데이트하고 로그를 기록하는 두 가지 작업이 모두 성공적으로 완료되어야 할 때 트랜잭션을 사용하여 두 작업을 하나의 단위로 처리합니다.
  3. 응용 프로그램 로직과 통합: 다른 시스템과의 통합이나, 다른 계층 간의 데이터 흐름과 조정을 담당합니다. 예를 들어, 외부 시스템에서 데이터를 가져오고, 필요한 데이터 변환을 수행한 후, 내부 데이터베이스에 저장하는 과정을 총괄할 수 있습니다.
  4. 보안과 권한 검증: 요청이 유효한지 확인하고, 사용자가 요청을 수행할 권한을 가지고 있는지 검증하는 로직을 구현합니다.

예시: 회원 등록 프로세스

  • Repository 메서드: save(Member member)
  • Service 메서드: registerMember(MemberDTO memberDto)
    • DTO(Data Transfer Object)에서 도메인 객체로 변환
    • 유효성 검사 실행
    • 중복 회원 검증
    • 회원 저장 (Repository 호출)
    • 추가 로직 수행 (예: 환영 이메일 발송)

Service 계층은 이렇게 다양한 작업을 조율하고, Repository 계층은 데이터베이스와의 상호작용을 담당하는 데 초점을 맞추고 있습니다. 이러한 분리는 코드의 관리와 유지보수를 용이하게 하며, 각 계층을 독립적으로 테스트할 수 있는 환경을 제공합니다.

코드에서 MemberRepository를 인터페이스로 작성하고 MemoryMemberRepository라는 구현체를 별도로 만든 이유는 주로 유연성, 확장성 및 테스트 용이성을 높이기 위한 것입니다. 다음은 이러한 설계 선택의 주요 이유들입니다:

  1. 유연성과 확장성: 인터페이스를 사용하면 다양한 저장소 구현을 같은 인터페이스로 다룰 수 있습니다. 예를 들어, 처음에는 메모리 기반 저장소를 사용하다가 나중에 데이터베이스 기반의 구현체로 쉽게 전환할 수 있습니다. 이를 통해 애플리케이션의 나머지 부분은 변경 없이 다양한 구현체를 자유롭게 교체하거나 업그레이드할 수 있습니다.
  2. 분리된 관심사(Separation of Concerns): 인터페이스를 사용함으로써 저장소의 구현 세부 사항을 사용하는 클라이언트 코드로부터 분리할 수 있습니다. 이는 코드의 각 부분이 자신의 관심사에만 집중할 수 있게 해줍니다. 예를 들어, 비즈니스 로직은 데이터가 어떻게 저장되고 관리되는지 알 필요 없이 데이터에 대한 연산을 요청할 수 있습니다.
  3. 테스트 용이성: 인터페이스를 사용하면 구현체를 모의 객체(Mock) 등으로 쉽게 대체할 수 있어 테스트가 간편해집니다. 이는 특히 단위 테스트에서 유용합니다. 테스트 중에는 실제 데이터 저장소에 접근하지 않고도 저장소를 사용하는 로직을 검증할 수 있습니다.
  4. 디자인 패턴의 적용: 인터페이스는 다양한 디자인 패턴, 예를 들어 전략 패턴(Strategy Pattern), 팩토리 패턴(Factory Pattern), 데코레이터 패턴(Decorator Pattern) 등의 적용을 용이하게 합니다. 각각의 패턴에서 인터페이스는 다양한 구현을 동적으로 교체하는 데 필요한 유연성을 제공합니다.
  5. 의존성 주입(Dependency Injection) 용이성: 인터페이스를 사용하면 구현체를 외부에서 주입하는 것이 간편해집니다. 이는 코드의 결합도를 낮추고, 의존성 관리를 더욱 효율적으로 할 수 있게 해줍니다.

이와 같은 이유로 인터페이스를 활용하는 것은 다양한 저장소 구현을 효과적으로 관리하고, 시스템의 전반적인 유지보수성과 확장성을 개선하는 데 도움이 됩니다.

package hello.hellospring.repository; // 패키지 선언, 코드의 네임스페이스를 정의합니다.

import hello.hellospring.domain.Member; // Member 클래스를 임포트합니다. Member 도메인 모델에 접근하기 위함입니다.

import java.util.List; // 자바 유틸리티에서 List 인터페이스를 임포트합니다.
import java.util.Optional; // 자바 유틸리티에서 Optional 클래스를 임포트합니다. 값이 없을 수도 있는 객체를 감싸 처리하기 위해 사용됩니다.

public interface MemberRepository { // MemberRepository 인터페이스 정의 시작
    Member save(Member member); // 회원 정보를 저장하고 저장된 정보를 반환하는 메서드
    
    Optional<Member> findById(Long id); // ID로 회원을 찾고 결과를 Optional로 감싸서 반환하는 메서드
    
    Optional<Member> findByName(String name); // 이름으로 회원을 찾고 결과를 Optional로 감싸서 반환하는 메서드
    
    List<Member> findAll(); // 모든 회원의 목록을 반환하는 메서드
}

 

package hello.hellospring.repository; // 패키지 선언

import hello.hellospring.domain.Member; // Member 클래스를 임포트합니다.
import java.util.*; // 자바 유틸 패키지에서 모든 클래스를 임포트합니다.

public class MemoryMemberRepository implements MemberRepository{ // MemberRepository 인터페이스를 구현하는 MemoryMemberRepository 클래스

    private static Map<Long, Member> store = new HashMap<>(); // 회원 정보를 저장할 HashMap 객체 생성. 모든 인스턴스가 공유하는 static 변수
    private static long sequenc = 0L; // 회원 ID를 생성하기 위한 sequence, static으로 모든 인스턴스가 공유

    @Override
    public Member save(Member member) { // 회원 저장 메서드 구현
        member.setId(++sequenc); // 회원 ID 자동 증가
        store.put(member.getId(), member); // HashMap에 회원 ID와 회원 정보 저장
        return member; // 저장된 회원 정보 반환
    }

    @Override
    public Optional<Member> findById(Long id) { // ID로 회원 찾는 메서드 구현
        return Optional.ofNullable(store.get(id)); // HashMap에서 ID로 회원 정보 검색, 결과가 null일 수 있으므로 Optional로 감싸 반환
    }

    @Override
    public Optional<Member> findByName(String name) { // 이름으로 회원 찾는 메서드 구현
        return store.values().stream() // HashMap의 값 목록을 스트림으로 생성
                .filter(member -> member.getName().equals(name)) // 스트림에서 회원 이름이 입력 이름과 일치하는지 필터링
                .findAny(); // 일치하는 첫 번째 회원을 반환, 없으면 Optional.empty 반환
    }
    @Override
    public List<Member> findAll() { // 모든 회원 목록 반환 메서드 구현
        return new ArrayList<>(store.values()); // HashMap의 값 목록을 ArrayList로 변환하여 반환
    }
}