보글보글 개발일지
article thumbnail
반응형

섹션 1. 프로젝트 환경설정

- devtools를 추가하고,

implementation 'org.springframework.boot:spring-boot-devtools'

 

recompile을 하면 서버 다시 켜지 않아도 html에서 바뀐 내용 적용된다.

- sql 실행 파라미터를 로그로 남긴다.

org.hibernate.type: trace

섹션 2. 도메인 분석 설계 (발표)

도메인 모델과 테이블 설계

- 다대다 관계 : 한 번 주문할 때 상품을 여러 개 주문 가능. 상품 하나도 주문 여러 개에 담길 수 있으므로 다대다 관계이다. 이는 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다로 풀어낸다.  

- Member-Order을 동급으로 생각. 회원을 통해서 주문 일어나는게 아니라 주문 생성할 때 회원 필요하다.라고 보는게 맞다.
- Member-Order: 일대다, 다대일의 양방향 관계. 연관관계의 주인은 FK 있는 주문으로 하는 것이 좋다. 값 변경은 주인쪽. 반대쪽은 단순히 조회용.

---> 데베 내용, 기본편 내용이라 나중에 JPA 기본편 듣고 다시 한 번 정리해야 할 것 같음.

엔티티 클래스 개발1

Getter: 열어두고, Setter은 꼭 필요한 경우에만 사용하는 것을 추천. Setter열어두면 데이터 변경이 발생할 수 있다.

- 일대다, 다대다의 경우: 객체는 변경포인트가 두군데이지만, 테이블에서는 Foreign Key만 바꾸면 되므로, 둘 중 하나를 주인으로 정해야한다. 연관관계 주인. Orders나 Member중 Foreign Key가 가까운 것을 주인으로 정한다.

//Member.java
    @OneToMany(mappedBy = "member") //멤버랑 주문사이 관계:멤버 입장에서는 하나의 회원이 여러개 상품 주문--> 일대다 관계
    //주인이 아니라 거울이다!를 적어준다.--> mappedBy 써주고, order 테이블의 member에 의해 매핑 된것.

//Order.java
    @ManyToOne
    @JoinColumn(name="member_id") //주문한 회원에 대한 정보 매핑
    private Member member; //멤버랑 관계 설정 - 다대일 관계 ; 반대로 멤버 입장에서는 하나의 회원이 여러개 상품 주문--> 일대다 관계

- Enum의 경우 ORDINAL, STRING 두가지 타입이 있는데, STRING으로 써야한다. ORDINAL의 경우 숫자로 1, 2, 3...이 들어가는데 중간에 다른 상태가 생기면 망한다.

//Delivery.java
@Enumerated(EnumType.STRING) //string으로 꼭 써야한다. ordinal으로 넣으면 숫자로 들어간다. 중간에 다른 상태 생기면 1, 4, 2 이런식으로 되어 망한다.
private DeliveryStatus status; //READY, ORDER

-일대일 관계의 경우, FK를 어디다 둬도 상관 없는데, 접근 많이 하는 곳에 FK를 두는 것이 편하다. 따라서 Order에 FK를 둔다.--> 주인!

//Order.java
@OneToOne
@JoinColumn(name="delivery_id")
private Delivery delivery;
//Delivery.java
@OneToOne(mappedBy="delivery")
private Order order; //one to one에서는 foreign 키 어디다 둬도 상관 없는데, 접근 많이 하는 곳에 foreign키 두는걸 선호하심.

-item안에 세부 사항을 추가할 때 상속을 해준다. 이때 strategy를 정해야하는데, SINGLE_TABLE의 경우 한 테이블에 전부 다 때려 넣는 것이다. 추가로 TABLE_PER_CLASS, JOINED와 같은 것이 있다.

//item.java

@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //한 테이블에 다 때려 박음
@DiscriminatorColumn(name="dtype")

-SINGLE TABLE이기 때문에 Book, Album, Movie가 저장될 때 구분이 되어야한다. 따라서 DiscriminatorValue를 세팅해준다.

//Book.java
@DiscriminatorValue("B") //Single table 이라 저장될 떄 구분될 수 있어야하므로 넣는 값

엔티티 클래스 개발2

- ManyToMany는 원래 거의 안쓴다. 다대다는 필드 추가가 안된다. 따라서 복잡한 거 많은 실무에서 쓸 일 더더욱 없다.

//Item.java
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
//Category.java
@ManyToMany //예제일 뿐 필드. 다대다는 더 추가하는 거 안됨. 실무에선 복잡한거 많음.
@JoinTable(name="category_item",joinColumns = @JoinColumn(name="category_id"), inverseJoinColumns = @JoinColumn(name="item_id"))
//inverse는 category_item 테이블의 아이템 쪽으로 들어가는 것 매핑해줌
private List<Item> items = new ArrayList<>();

-Category의 계층 구조는 셀프로 양방향 연관관계를 만드는 방식으로 구현한다.

//Category.java
@ManyToOne
@JoinColumn(name="parent_id")
private Category parent; //카테고리의 계층 구조->셀프로 양방향 연관관계

@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();

-테이블은 관례상 테이블명 + id를 많이 사용한다.

  • 엔티티 식별자: id
  • PK 컬럼명: member_id

-Address의 경우 값 타입 변경이 불가능하게 설계해야 한다. Setter을 제공 하지 않고 생성자에서 값이 초기화 되도록 설계하는 것이 좋다.임베디드 타입, 엔티티는 protected로 기본 생성자를 두면 더 안전하다. => JPA 구현 라이브러리가 객체 생성할 때 리플랙션 같은 기술 사용할 수 있도록 지원하기 위해서!

Address.java
@Embeddable // JPA의 내장 타입이라는 것
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address(){
    }
    public Address(String city, String street, String zipcode) { //생성할때만 값 세팅, Setter 제공 안하는 게 좋음
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

엔티티 설계시 주의점

- 엔티티에는 가급적 Setter 사용하지 말자.

- 모든 연관관계는 지연 로딩으로 설정!

  • 즉시로딩: 멤버 조회할 때 연관된 오더를 한번에 다 조회. 로딩하는 시점에 다른 애들도 한번에 조회. 최악의 경우에 하나 가져오면 연관된 애들 다 가져온다. 다 LAZY로 가져와야한다.
  • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
  • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한 다.

- Order에 member와의 연관관계를 @ManyToOne(fetch = FetchType.EAGER)로 해두면(default) order조회할 때 member를 조인해서 같이 가져오게 된다.  위에 처럼 이상하게 해석된다. ->N+1문제. -->member을 꼭 같이 조회 하겠다는 얘기

섹션 4. 회원 도메인 개발

회원 레포지토리 개발

@PersistenceContext
private EntityManager em; //스프링이 엔티티 메니저 만들어서 주입해줌

 

public List<Member> findAll(){
    return em.createQuery("select  m from Member m", Member.class).getResultList();
    //jpql과 sql의 차이점이 좀 있다. jpql은 entity 객체를 대상으로 쿼리 진행
}

- jpql과 sql의 차이점이 좀 있다. jpql은 entity 객체를 대상으로 쿼리 진행. sql은 테이블을 대상으로 쿼리 진행.

회원 서비스 개발

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

- (MemberRepository.java)persist를 하면 이 순간에 영속성 context에 이 멤버 객체 올리는데, Key가 id값이 된다. 그래서 Member.java에서 @GeneratedValue를 하면 id값이 항상 생성되는게 보장된다. DB에 들어가는 시점이 아니여도 그렇게 해준다.

Member.java

@Transactional(readOnly = true)

- readOnly = true 로 주면 JPA가 조회하는 곳에서 성능을 더 최적화한다. 읽기에는 가급적 readOnly = true 를 붙혀주면 좋다. 다만 쓰기할 때 true를 주면 수정이 안된다.

@Transactional //기본이 readOnly = false

- 기본이 false이기 때문에 전체 Class에 True를 걸어주고, join과 같이 쓰기가 필요한 곳에는 False를 걸어주면 된다. 따로 설정한 건 그게 우선권을 가진다.

private void validateDuplicateMember(Member member) { //중복회원 검증 로직
    List<Member> findMembers = memberRepository.findByName(member.getName()); //동시에 회원가입하면 문제됨 --> member의 name에 unique 제약 조건 걸어야함
    if(!findMembers.isEmpty()){
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    }
}

-  동시에 회원가입하면 문제됨 --> member의 name에 unique 제약 조건 걸어야한다.

@Autowired
private MemberRepository memberRepository;

- test하거나 할때 멤버 레포 바꾸고싶은데 못바꾼다.

private MemberRepository memberRepository;

@Autowired //setter injunction
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

- Setter Injunction: setMemberRepository 선언하는 것.

  • 장점: test 코드 작성할 때 직접 주입 가능
  • 단점: 런타임에 누군가 바꿀 수 있음. 
    • 하지만 런타임에 바꿀일이 거의 없기 때문에 Setter쓰는 건 별로다.
private MemberRepository memberRepository
@Autowired
public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

- 위 단점 보완한 것이 생성자 주입이다. 생성자 주입을 쓰면 한 번 생성될 때 완성되므로 중간에 바꿀 수 없다. 테스트 케이스 작성할 때 아래처럼 뭔가 값을 넣어주지 않으면 빨간 불이 들어온다. 오류 수정 빠르게 할 수 있다.

- 하지만 이렇게 하면 코드가 복잡하다. 따라서 생성자가 딱 하나만 있는 경우, 스프링이 @Autowired가 없어도 자동으로 주입을 해준다.
- 그리고 변경할 일이 없기 때문에 final 선언을 해준다. final로 해두면, 아래처럼 생성자 키 값 세팅 안하면 컴파일 시점에 오류를 잡을 수 있기 때문에 final을 추천한다.

- 여기서 롬복을 써보자. @AllArgsConstructor는 필드 모든 걸 가지고 생성자 만든다.
- @RequiredArgsConstructor는 final이 있는 필드만 가지고 생성자를 만들어준다.

@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    
    ...
    
}

- 최종적으로 위와 같이 잘 쓴다.


- 앞서 진행했던 MemberRepository.java에서도 생성자 Injection이 가능하다. 스프링 부트의 JPA를 쓰면 

@PersistenceContext
private EntityManager em;

@Autowired
private EntityManager em;

 로 변경이 가능하다. 위는 또한 아래처럼 변경이 가능하다.

@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em; 
    ...
}

이렇게 하면 일관성 있고 간편하게 사용할 수 있다.

회원 기능 테스트

회원 기능 테스트 요구사항

  1. 회원 가입 성공
  2. 회원 가입 시 같은 이름이 있으면 예외 발생

- JPA가 실제 DB까지 도는 것을 확인하기 위해 메모리 모드로 DB까지 테스트하는게 중요. 아래 두가지 있어야 스프링이랑 integration 해서 테스트 가능.

  • @RunWith(SpringRunner.class)
  • @SpringBootTest

- @Transactional 있어야 롤백이 된다. --> 같은 Transaction 안에서 같은 Entity, 즉 ID값(PK값)이 똑같으면 같은 영속성 context에서 똑같은 애가 관리가 된다.
- 데이터 베이스에서 트랜잭션이 commit될 때 flush 되면서 insert 쿼리가 쫙 나간다. 근데 스프링에서는 기본적으로 @Transactional이 Test에 있으면 커밋이 아닌 rollback(DB에 있는 것 버림)을 해버린다. 따라서 @Rollback(false)를 주면 등록 쿼리를 다 볼 수 있다.

- Rollback이지만 DB에 쿼리 남기는 것을 보고싶다면?

@Autowired
EntityManager em;

위처럼 EntityManager을 불러온 후

em.flush();

영속성 context에 있는 변경이나 등록 내용 데이터 베이스에 반영해주면 된다. 그럼 insert문을 볼 수 있고, 실제 트랜잭션은 롤백된다.

- 반복 테스트 하기 때문에 롤백하는게 맞다. 데베에 데이터 남으면 안된다.
- 의심되면 Rollback(false) 하고 데이터 눈으로 확인해보면 된다.

public void 중복_회원_예외() throws Exception{
    //given
    Member member1= new Member();
    member1.setName("kim");
    Member member2= new Member();
    member2.setName("kim");
    //when
    memberService.join(member1);
    try{
        memberService.join(member2); //예외가 발생해야 한다.
    }catch(IllegalStateException e){
        return;
    }
    //then
    fail("예외가 발생해야 한다."); //코드가 돌다가 여기 오면 안됨
}

- 두번째 join에서 예외가 발생해 코드가 끝나버리는데, 이는 간단하게 한줄로 아래와 같이 작성할 수 있다. 기대하는 것이 어떤 예외인지 적어주면 된다.

@Test(expected =  IllegalStateException.class)

 - 위와 같이 test-resources-application.yml 파일을 만들고, 아래 코드로 적으면 메모리 모드로 동작한다.

url: jdbc:h2:mem:test

- 사실 스프링 부트는 별다른 설정이 없어도 자동으로 메모리 모드로 동작한다. 따라서 spring: 이하 코드가 없어도 된다.
- main, test의 appliction.yml을 따로 두는게 맞다. 
- ddl-auto: create는 가진 엔티티 다 drop한 다음에 create하고 동작시키고, create-drop는 마지막에 앱 종료 시점에 drop 쿼리 다 날려서 완전히 깨끗하게 초기화.

 

반응형
profile

보글보글 개발일지

@보글

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!