Member

package com.board.domain;

import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "member")
@Setter
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @NotBlank(message = "아이디를 입력해주세요.")
    @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "아이디를 3~12자로 입력해주세요. [특수문자X]")
    private String username;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "비밀번호를 3~12자로 입력해주세요.")
    private String password;
    private String email;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Board> board = new ArrayList<>();

    @Builder
    public Member(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
}

@Table -> 테이블 명은 member ,Id 는 PK 로 두고 AUTOINCREMENT 로 설정한다.

@NotBlank 와 @Pattern 은 validation 을 위한 어노테이션 -> 추후 구현

1:N 관계 이므로 @OneToMany 어노테이션.

@Builder 는 @Setter 를 두지 않기 위한 어노테이션. Setter 를 이용해서 값을 설정하는 것은 개발자의 의도와 다르게 값이 변경될 가능성이 있기에 사용한다. 기본 생성자는 protected 로 막는다.

 

 

 

Board

package com.board.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "board")
@Getter
@Setter
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdDate;
    private Long createdBy;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

 

Board 테이블은 기본적인 매핑만 해주고 추후 수정. (연관관계 매핑 필요)

우선 Member 로직 먼저 구현 후 , 테스트 할 예정이다.

 

 

MemberRepository

package com.board.domain.Repository;

import com.board.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public void saveMember(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m").getResultList();
    }

    public List<Member> findByName(String username) {
        return em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", username)
                .getResultList();
    }
}

JPA 를 이용하여 멤버 저장하는 쿼리와 하나의 id 찾기 , 전체 찾기 , 일치하는 이름(아이디) 찾는 메소드 각각 작성.

 

 

 

MemberService

package com.board.domain.Service;

import com.board.domain.Member;
import com.board.domain.Repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.saveMember(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getUsername());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 이름입니다.");
        }
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}

 

@Transactional 에서 옵션을 read only 로 주면 쿼리 성능 향상.

다만 기본적인 조회가 아닌 영속성 컨텍스트에 값이 들어가야 하는 경우 @Transactional 어노테이션을 해당 메소드에 작성한다.

@Transactional read only 를 클래스 레벨에 적용하면 모든 메소드에 read only 옵션이 적용된다. -> 필요한 메소드만 @Transactional 사용한다.

 

 

메모리 안에서 테스트 하기

## H2 설정
#spring.datasource.url=jdbc:h2:mem:test
#spring.datasource.username=sa
#spring.datasource.password=
#spring.datasource.driver-class-name=org.h2.Driver
#
## JPA 설정
#spring.jpa.hibernate.ddl-auto=create
#spring.jpa.properties.hibernate.format_sql=true


# Log 설정
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type=trace

url=jdbc:h2:mem:test 로 기존 설정과 다르게 해주면 테스트 시에는 메모리 안에서 수행한다. 또한, 스프링 부트는 따로 데이터베이스를 설정 해주지 않으면 자체 기능으로 메모리를 사용해 테스트를 수행한다.

 

스프링 부트는 create-drop을 사용하기 때문에 테스트가 끝나면 drop을 하여 메모리를 정리한다.

 


MemberServiceTest

package com.board.domain.Service;

import com.board.domain.Member;
import com.board.domain.Repository.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;


@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    MemberRepository memberRepository;

    @Test
    void register() {
        Member member = Member.builder()
                .username("kim")
                .password("1234")
                .email("email")
                .build();

        Long joinId = memberService.join(member);
        Assertions.assertThat(memberRepository.findOne(joinId)).isEqualTo(member);
    }

    @Test
    void dup_register() {

            //given
        Member member = Member.builder()
                .username("aim")
                .password("4321")
                .email("email")
                .build();

        Member member2 = Member.builder()
                .username("aim")
                .password("4321")
                .email("email")
                .build();
            //when
        try {
            memberService.join(member);
            memberService.join(member2);
        } catch (IllegalStateException e) {
            assertEquals(e.getMessage(), "이미 존재하는 이름입니다.");
            System.out.println(e.getMessage());
            return;
        }

            //then
        Assertions.fail("예외가 발생해야 한다.");
        
        // Junit5
       // assertThrows(IllegalStateException.class, () -> memberService.join(member2));

    }

}

 

 

void 회원_가입 : 회원 가입이 정상적으로 이루어지는지 확인하는 테스트

 

Setter 를 두지 않고 @Builder 를 사용해서 값을 저장한다.

 

※ 주의 : 테스트 케이스에서는 항상 데이터가 롤백되기 때문에 @Commit 어노테이션을 적어주지 않으면 값이 저장되지 않는다. 하지만 중복 회원 가입 테스트에서는 @Commit을 지워주지 않으면

"Transaction silently rolled back because it has been marked as rollback-only" 에러가 발생한다.

 

중복 테스트에서 사용한 IllegalStateException은 RuntimeException을 상속한 예외로 unchecked exception으로 분류 되어 rollback 해야만 하는데 강제로 Commit 하니 위와 같은 에러가 발생한 것이다.

 

이렇게 중복 검사에 걸린 부분은 오히려 db에 저장이 안 되는 로직이 맞으니 강제로 rollback하는 @Rollback이나 @Commit 을 지워주면 된다.

 

복사했습니다!