Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바의 정석 기초편 ch3
- jpa 활용2 - api 개발 고급
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch4
- 스프링 고급 - 스프링 aop
- 게시글 목록 api
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch5
- 스프링 mvc2 - 타임리프
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch8
- 스프링 db2 - 데이터 접근 기술
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc1 - 서블릿
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch2
- 스프링 입문(무료)
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch12
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch6
- @Aspect
Archives
- Today
- Total
나구리의 개발공부기록
애플리케이션 구현 준비 - 구현 요구사항 및 애플리케이션 아키텍처, 회원 도메인 개발, 회원 리포지토리 개발, 회원 서비스 개발, 회원 기능 테스트, 상품 도메인 개발, 상품 엔터티 개발 - 비즈니스 로직 추가, 상품 리포지토리 개발, 상품 서비스 개발 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
애플리케이션 구현 준비 - 구현 요구사항 및 애플리케이션 아키텍처, 회원 도메인 개발, 회원 리포지토리 개발, 회원 서비스 개발, 회원 기능 테스트, 상품 도메인 개발, 상품 엔터티 개발 - 비즈니스 로직 추가, 상품 리포지토리 개발, 상품 서비스 개발
소소한나구리 2024. 9. 29. 12:43출처 : 인프런 - 실전! 스프링 부트와 JPA활용1 - 웹 애플리케이션 개발(유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 애플리케이션 구현 준비 - 구현 요구사항 및 애플리케이션 아키텍처
1) 구현 요구 사항
(1) 회원 기능
- 가입, 조회
(2) 상품 기능
- 등록, 수정, 조회
(3) 주문 기능
- 주문, 취소
- 주문내역 조회
2) 예제를 단순화를 위한 기능 간소화
- 로그인과 권한관리 구현 X
- 파라미터 검증과 예외처리는 단순화
- 상품은 도서만 사용
- 카테고리, 배송 정보는사용 X
3) 애플리케이션 아키텍처
(1) 계층형 구조 사용
- controller, web: 웹 계층
- service: 비즈니스 로직, 트랜잭션 처리
- repository: JPA를 직접 사용하는 계층, 엔터티 매니저 사용
- domain: 엔터티가 모여 있는 계층, 모든 계층에서 사용
- controller에서 repository에서 간단한 정보는 가져올 수 있도록 단방향으로 실용적인 구조를 구성(서비스에서 단순히 위임하는 로직을 최대한 제거하기 위한 조치)
(2) 패키지 구조
- jpabook.jpashop
- domain
- exception
- repository
- service
- web
(3) 개발 순서
- 리포지토리, 서비스계층을 개발
- 테스트 케이스를 작성해서 검증
- 마지막에 웹 계층 적용
2. 회원 도메인 개발 - 회원 리포지토리 개발
1) 기술 설명
- @Repository: 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
- @PersistenceContext: 엔터티 매니저(EntityManager)를 주입, 스프링이 자동 주입해주므로 생략 가능
- @PersistenceUnit: 엔터티 매니저 팩토리를 직접 주입할 때 사용 - 쓸일이 거의 없음
- JPQL - 객체(엔터티)를 대상으로 하는 쿼리문, SQL과 거의 비슷
3) 기능 설명
- save(): 멤버 저장
- findOne(): 단건 조회
- findAll(): 전체 조회, JPQL사용
- findByname(): 이름으로 조회: JPQL사용
package jpabook.jpashop.repository;
@Repository
public class MemberRepository {
private EntityManager em;
// 저장
public Long save(Member member) {
em.persist(member); // 저장
return member.getId();
}
// 하나 조회
public Member findOne(Long id) {
return em.find(Member.class, id);
}
// 전체 조회
public List<Member> findAll() {
// JPQL - 엔터티를 대상으로 쿼리
return em.createQuery("select m from Member m", Member.class).getResultList();
}
// 이름으로 조회
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
3. 회원 도메인 개발 - 회원 서비스 개발
1) 기술 설명
- @Service: 컴포넌트 스캔 대상이되어 자동으로 스프링 빈으로 등록이 됨
- @Transactional: 트랜잭션 적용(영속성 컨텍스트)
- readOnly=true: 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 않으므로 약간의 성능 향상(읽기 전용에는 다 적용 됨)
- 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
- @Autowired: 생성자 Injection, 생성자가 하나면 생략 가능
** 참고 사항
- 실무에서는 검증 로직이 있어도 정말 동시에 동일한 이름으로 회원이 가입을 시도할 수 있으므로 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전함
package jpabook.jpashop.service;
@Service
// 기본적으로 JPA는 트랜잭션으로 수행됨
@Transactional(readOnly = true) // 전체적으로는 읽기전용으로 동작(조회 기능이 더 많아서 클래스 레벨의 트랜잭션을 읽기 전용으로 적용)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 회원 가입
@Transactional // 회원 가입에는 쓰기도 가능하도록 설정
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId(); // 반환값은 id만
}
// 중복 회원 검증 메서드
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다");
}
// 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전함
}
// 회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Member findOne(Long id) {
return memberRepository.findOne(id);
}
}
2) 필드 주입, 생성자 주입
(1) 생성자 주입방식을 사용 - 롬복을 사용하여 최신 트렌드를 적용
- 스프링 필드 주입 대신 생성자 주입을 사용하며 세터 주입은 사용 금지
- 생성자 주입을 사용하면 변경 불가능하고 안전한 객체 생성이 가능함
- 생성자가 하나일 경우 @Autowired를 생략할 수 있음
- final 키워드를 추가하면 컴파일 시점에 memberRepository를 설정하지 않는 오류를 체크할 수 있음(보통 기본 생성자를 추가할 때 발견함)
- 롬복의 @RequiredArgsConstructor를 사용하면 final이 붙은 필드의 생성자를 자동으로 생성해줌
// 필드 주입
@Autowired
MemberRepository memberRepository;
// 세터 주입 - 사용 X, 테스트 사용시 Mock Repository등을 사용할 수 있으나 변경의 위험이 너무 큼
private MemberRepository memberRepoisoty;
@Autowired
public void setMemberRepository)MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
// 생성자 주입
private MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 실제 코드에 적용한 lombok을 적용한 가장 권장하는 방법
@RequiredArgsConstructor // final이 붙어있으는 필드의 생성자를 만들어줌
public class MemberService {
private final MemberRepository memberRepository;
}
(2) MemberRepository 코드에도 적용
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
// 기타 코드 생략
}
3. 회원 기능 테스트
1) MemberServiceTest - junit5사용
- junit의 Assertions를 검증으로 사용
- @SpringBootTest: 스프링 부트 띄우고 테스트(이게 없으면 @Autowired가 다 실패함)
- @Transactional: 반복 가능한 테스트를 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백함(테스트에서 @Transactional을 사용했을 때)
- 정상적인 회원가입과, 중복 회원가입으로 예외가 발생했을때의 테스트를 검증
- Given, When, Then 테스트 케이스 작성법
package jpabook.jpashop.service;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
// Given
Member member = new Member();
member.setName("kim");
// When
Long saveId = memberService.join(member);
// Then - 검증
assertEquals(member, memberRepository.findOne(saveId));
}
@Test
public void 중복_회원_예외() throws Exception {
// Given
Member member1 = new Member();
Member member2 = new Member();
member1.setName("lee");
member2.setName("lee");
// When
memberService.join(member1);
// Then
// member2를 join하려고 할 때 IllegalStateException이 발생해야 함
assertThrows(IllegalStateException.class,
() -> memberService.join(member2));
}
}
2) 테스트 설정 - 임베디드 DB모드
(1) 테스트 격리
- 테스트는 격리된 환경에서 실행하고 끝나면 데이터를 초기화하는 것이 좋기 때문에 메모리 DB를 사용하는 것이 가장 이상적임
- 그리고 테스트 케이스를 위한 스프링 환경과 애플리케이션을 실행하는 환경은 보고싶은 로그레벨 등의 설정이 다를 수 있으므로 설정파일을 다르게 적용하는 것이 맞음
- 스프링 부트는 datasource 설정이 없으면 기본적으로 메모리 DB를 사용하고 driver-class도 현재 등록된 라이브러리를 보고 찾아주며 ddl-auto 설정도 create-drop(테이블 생성후 테스트 종료되면 삭제하도록 동작하는 방식) 모드로 적용하여 동작하기 때문에 데이터소스, JPA 관련 설정등을 하지 않아도 잘 동작함
(2) 적용 방법
- ~test/java 경로에 resources 디렉토리를 생성
- 기존에 작성했던 application.yml을 복사하고 db관련 내용을 전부 주석처리하고 테스트를 실행하면 테스트가 메모리모드로 정상적으로 동작함(H2 데이터베이스를 실행할 필요없이 테스트가 동작)
- 만일 해당 위치에 application 설정 파일이 없다면 src/main/resources/위치의 설정파일을 읽음
- https://nagul2.tistory.com/314
spring: # 띄어쓰기 없음
# datasource: # 띄어쓰기 2칸
# url: jdbc:h2:tcp://localhost/~/jpashop # 띄어쓰기 4칸
# username: sa
# password:
# driver-class-name: org.h2.Driver
#
# jpa: # 띄어쓰기 2칸
# hibernate: # 띄어쓰기 4칸
# ddl-auto: create # 띄어쓰기 6칸
# properties:
# hibernate:
## show_sql: true # 띄어쓰기 8칸, System.out으로 출력 -> 실무에서는 사용안함
# format_sql: true # Log로 출력
logging: # 띄어쓰기 없음
level: # 띄어쓰기 2칸
org.hibernate.SQL: debug # 띄어쓰기 4칸 -> JPA 쿼리 출력
org.hibernate.orm.jdbc.bind: trace # SQL에서 파라미터값이 ?로 나오는데 그것을 실제 값으로 출력
4. 상품 엔터티 개발 - 비즈니스 로직 추가
- 도메인 주도 설계를 할때 엔터티 자체에서 해결할 수 있는 것들은 엔터티 안에 비즈니스 로직을 넣는 것이 좋으며 객체 지향적인 설계이며 응집도가 있는 설계를 할 수 있음 - information expert pattern을 지키면서 개발
- 도메인 주도 설계를 하게되면 많은 비즈니스 로직이 엔터티로 이동하게 되어 엔터티를 객체로 사용하는 것이고 서비스에 비즈니스 로직이 모두 있다면 엔터티는 자료구조로 사용하는 방식이며 둘 방식에는 장단점이 존재하기에 상황에 맞는 적절한 방법을 택해야함
- 값을 변경해야 할 일이 있다면 지금 예제에서는 편의를 위해 setter를 사용하고 있지만 핵심 비즈니스 메서드를 가지고 값을 변경하는 것이 좋음
- 세터를 사용해 바깥에서 계산해서 입력하는 것이 아니라 엔터티 안에서 변경을 위한 메서드가 있는것이 가장 객체지향 적이라고 볼 수 있음
- 클린코드 | 로버트 C. 마틴 도서 - 6. 객체와 자료구조 추천
1) Item - 비즈니스 로직 추가
- addStock() : 파라미터로 넘어온 수만큼 재고를 늘리는 메서드, 재고가 증가하거나 상품 주문을 취소해서 재고를 다시 늘려야할 때 사용
- removeStock() : 파라미터로 넘어온 수만큼 재고를 줄이는 메서, 재고가 부족하면 예외를 발생시며 주로 상품을 주문할 때 사용함
- NotEnoughStockException() : 직접 정의한 RuntimeException
// 애노테이션 생략
public abstract class Item {
// 기존 코드 생략
/* 비즈니스 로직 */
// 재고 증가
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
// 재고 감소
public void removeStock(int stockQuantity) {
int restStock = this.stockQuantity - stockQuantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
}
2) NotEnoughStockException - 예외 추가
package jpabook.jpashop.exception;
public class NotEnoughStockException extends RuntimeException {
public NotEnoughStockException() {
super();
}
public NotEnoughStockException(String message) {
super(message);
}
public NotEnoughStockException(String message, Throwable cause) {
super(message, cause);
}
public NotEnoughStockException(Throwable cause) {
super(cause);
}
}
5. 상품 리포지토리 개발
1) ItemRepository
- save() id가 없으면 신규로 간주하여 persist()로 저장하고 id가 있다면 merge()로 저장
- merge()에 대한 자세한 내용은 뒤에서 설명, 지금은 데이터베이스에 저장된 엔터티를 가져와서 수정하는 update와 비슷하게 동작한다는 정도로만 이해
- 단건 조회하는 findOne()와 전체를 조회하는 findAll()를 생성
package jpabook.jpashop.repository;
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item); // id값이 없으면 저장
} else {
em.merge(item); // update와 비슷하며 DB에 저장된 엔터티를 수정한다고 이해하면됨 -> 자세한 내용은 이후에 설명
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item i", Item.class).getResultList();
}
}
6. 상품 서비스 개발
1) ItemService
- 지금의 상품 서비스는 상품 리포지토리에 단순히 위임하는 로직들로 구성되어 있음
- 경우에 따라서 이렇게 정말 위임만 하는 거에 대해서 만들어야할 필요성이 있는지에 대해 고민해볼 필요성이 있으며 이러한 경우에는 컨트롤러에서 아이템 리포지토리에 바로 접근해서 써도 큰 문제가 없다고 볼 수 있음 (김영한님 생각)
- 클래스 레벨의 트랜잭션에는 readOnly를 적용하고, 상품을 저장하는 saveItem() 에만 쓰기도 가능한 트랜잭션을 적용
package jpabook.jpashop.service;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long id) {
return itemRepository.findOne(id);
}
}
** 상품 기능 테스트는 회원 테스트와 로직이 비슷하므로 생략