Java Spring Framework

회원 관리 기능을 구현하면서 Business Logic에 대해 살펴보자.

Business Logic

일반적으로 application에서 기능을 수행할떄, controller, repository, domain, service의 개념이 적용된다. 사용자가 어떠한 기능을 요청했을때 controller은 이를 보고 service를 호출 하고, service는 repository를 이용해서 해당 기능을 수행한다.

Domain

controller, service, repository에서 비지니스 도메인 객체로 해당 비지니스 로직을 수행하면서 활용되는 객체이다.

public class Member {
    private Long id;
    private String name;

    public String getName() {
        return name;
    }

    public Long getId() {
        return id;
    }

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

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

Controller

특정 domain(객체)에 대한 기능 수행을 요청받았을때, 적절한 기능을 수행하기 위해 필요한 service를 호출한다.

@Controller
public class MemoryController {
    private final MemberService memberService;

    @Autowired
    public MemoryController(MemberService memberService) {
        this.memberService = memberService;
    }
}

Service

해당 기능을 직접적으로 수행하는 비지니스 로직 부분을 담당한다. 이때, service는 repository를 이용해서 기능 수행 및 DB에 접근한다.

public class MemberService
{
    private final MemberRepository memberRepository;
    //아래와 같이 객체 생성자 호출시 외부에서 전달된 인자로 초기화하는 것을 DI(Dependency Injection)이라고 한다.
    public MemberService(MemberRepository memberRepository){
        this.memberRepository=memberRepository;

    }
    /**
     * 회원가입
     */
     //보시다시피, Service에서는 repository를 이용해서 DB에 접근한다.
    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }
    /**
    * 중복된 이름을 가지고 회원가입을 수행하려고 하면 에러를 발생시킨다.
    */
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }
    /**
     * 전체회원조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }
    /**
     * 특정 회원조회
     */
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

Repository

DB에 저장/조회/삭제, 등의 작업을 수행한다.

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name0);
    List<Member> findAll();
}

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long,Member> store=new HashMap<>();
    private static long sequence=0L;

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

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

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return  new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

Test

코드를 작성하는 것 만큼이나, 혹은 그 이상으로 중요하는 것은 그 코드에 대한 검증이다. 구현의 시간보다 오히려 테스트 과정이 시간이 더 많이 걸리는 경우가 많다. 테스트를 정확하게 수행해야 좋은 양질의 코드를 생성할 수 있다. 자바 프로그램의 경우 테스트를 진행하기 위해 junit 프레임워크를 지원한다.

test code 구조 설계는 메인 code 설계 구조와 유사하게 가져가면 된다.

Member Repository Test


public class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository=new MemoryMemberRepository();
    
    //AfterEach annotation이 부여된 function은 매 테스트 이후에 실행된다.
    //매 테스트마다 DB를 비워줘야 객체 생성 및 검증이 올바르게 이루어 진다.
    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

    @Test
    public void save(){
        Member member=new Member();
        member.setName("spring");

        repository.save(member);
        Member result=repository.findById(member.getId()).get();

        //객체를 생성해서 DB에 저장한 후 이를 가져온 객체와 기존의 객체와 서로 같은 비교한다.
        //이때 Assertions 관련 class를 이용해서 검증을 수행한다.
        assertThat(member).isEqualTo(result);

    }

    @Test
    public void findByName(){
        Member member1=new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2=new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result=repository.findByName("spring1").get();

        assertThat(result).isEqualTo(member2);
    }

    @Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> results = repository.findAll();

        assertThat(results.size()).isEqualTo(2);
    }
}

Member Service Test

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    //각 test을 수행하기 이전에 먼저 beforeEach가 수행된다.
    @BeforeEach
    public void beforeEach(){
        //생성된 Member Repository와 Member Service 내부에서 사용되는 Repository를 동일하는 것으로 이용하기 위해 아래와 같이 Member Repository 객체를 Member Service로 전달해준다 --> DI 개념이 적용됨.
        memberRepository=new MemoryMemberRepository();
        memberService=new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void join() {
        //given
        Member member=new Member();
        member.setName("hello");
        //when
        Long saveId= memberService.join(member);

        //then
        Member findMember=memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());

    }

    //에러를 발생시키는 코드에 대해서도 테스팅을 할 필요가 있다. 
    /*에러를 검증하기 위해 Try-Catch 구문을 활용해도 되지만, assertThrow를 활용하는 것이 더 좋은 방법이다.
    추가로 에러 메세지를 검증하는 부분도 구현한다.
    */
    @Test
    public void duplicate_join()
    {
        //given
        Member member1=new Member();
        member1.setName("spring");

        Member member2=new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        IllegalStateException e= assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

   /*
        //중
        try{
            memberService.join(member2);
        }catch(IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }*/

        //then


    }
    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

Business Logic Dependency

비지니스 로직에 핵심적인 부분인 controller, service, repository, domain은 서로 의존관계를 가진다. 따라서 해당 의존관계를 명시하는 부분을 적용해야된다. 이러한 의존관계를 설정하는 방식은 크게 두가지로 나눠지게 된다.

Component Scan

Component Scan 방식은 annotation으로 설정된 클래스들을 보고 java spring에서 자동으로 의존관계를 엮어준다. 의존관계 설정이 필요한 클래스에 @Autowired을 설정하게 되면 java spring에서 spring container에 있는 spring bean를 찾아서 의존관계를 맺어준다.

Controller

@Controller
public class MemoryController {
    private final MemberService memberService;

    @Autowired
    public MemoryController(MemberService memberService) {
        this.memberService = memberService;
    }

Service

@Service
public class MemberService
{
    private final MemberRepository memberRepository;
    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository=memberRepository;

    }
    .
    .
    .

Repository

@Repository
public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long,Member> store=new HashMap<>();
    private static long sequence=0L;
    .
    .
    .

위 와 같이 각각 @Controller, @Service, @Repository로 설정되어 있고, 의존관계 설정이 필요한 부분은 @Autowired으로 설정해서 자동으로 의존관계가 설정될 수 있도록 한다.

정형화된 방식으로 구현을 하는 경우 위의 component scan 방식을 활용하는 것이 좋다.

Manual Configuration

manual configuration은 말 그대로 개발자가 직접 의존 관계를 코드로 부여하는 것이다. @Controller, @Service 와 같은 annotation을 이용하지 않고 직접 객체들을 spring bean으로 등록하고 의존관계를 설정한다.

SpringConfig

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        return  new MemoryMemberRepository();
    }
}

위와 같이 Member Service, Member Repository를 @Bean annotation을 통해 Spring Bean으로 등록한다. 정형화된 개발 방식이 않거나, 상황에 따라서 클래스를 변경해야하는 경우 이렇게 직접 의존 관계를 나타내기도 한다.

References

link: inflearn

댓글남기기