728x90

멀티 모듈이란?


 

모듈은 하나의 독립적인 배포 단위를 의미하기 때문에, 각 모듈은 build 과정에서 jar파일을 배포한다.

따라서, 멀티 모듈은 하나의 프로젝트 내에서 여러 개의 모듈을 가짐으로써, 각 모듈간의 역할, 의존성을 분리하는 것이다.

 

각 모듈을 나누는 방법


 

 

멀티 모듈을 통해 모듈 간의 역할과 의존성을 분리하였다.

Hexaonal구조에서 격리된 모듈의 기준은 도메인(비즈니스 로직)을 통해 구분된다. 그렇기에 도메인을 중심으로 설계하는 DDD방식이 주로 사용된다.

 

DDD가 MSA설계에 사용되는 이유는 무엇인가?

  • 유비쿼터스 언어 : 복잡한 도메인을 정의하기 위해서는 SW개발자 외에도 도메인 전문가가 필요하다. 도메인 전문가와 SW개발자 모두 이해할 수 있는 공통 언어를 사용한다.
  • 애그리게잇: 객체들의 그룹을 단일 단위로 취급하여, 서비스의 일관성을 유지한다. 객체는 엔터티로 치환되며, 엔터티를 그룹화한 단위가 애그리게잇이고 이는 하나의 서비스와 일치한다.
  • Bounded Context: 도메인 간의 관계에서 각 도메인마다 배타적인 문제를 해결해야 한다. 따라서 Bounded context에 따라 하위 도메인 (Sub-domain)을 분리한다.
  • Context Mapping: Bounded Context간의 상호작용 및 통합을 정의한다.

 

 

 

728x90

모놀리식과 MSA 구조


 

MSA가 등장하기 전에는 모놀리식이라는 구조라고 할 것도 없이 모놀리식 구조로 개발해왔다고 한다.

단, 프로젝트의 규모가 커짐에 따라 하나의 어플리케이션을 도메인 성격에 따라 여러 개의 작은 서비스로 나누어 개별적으로 관리하는 MSA구조가 등장하게 되었다.

Hexagonal 구조 vs Layered 구조 


모놀리식이건 MSA건 각 서비스가 가져야할 구조는 어떤 것들이 있을까?

먼저 모놀리식 아키텍처가 개발의 기본이었던 것과 마찬가지로 Layered 구조(오른쪽)이 가장 기본적인 형태의 구조였다. Layered 구조는 데이터베이스를 중심으로 중심으로 설계했다. HTTP요청(User)를 DB에 저장하는 방법에 대해 고민하였고, 이를 가장 명료하게 구현하는 방식은  MVC패턴이 대표적인 디자인 패턴이 아닐까 싶다.

 

 

그러나 MSA가 등장한 이유와 같이, 요즘의 복잡해진 어플리케이션들은 비즈니스 로직이 수시로 변경되기도 하고 추가적인 종속성이 끊임없이 발생한다. 이러한 과정에서 Layered 구조는 종속성 혼란을 겪기도 한다.

 

 

이에 대한 대비책은 Hexagonal 구조를 채택하는 것이다. Hexagonal 구조는 비즈니스 로직을 중심으로 외부와 내부를 격리한다. 즉, 내부에 비즈니스 로직을 수행하는 계층을 두고 Port를 통해 외부와 통신하며 동작한다.

 

 

결과적으로 Hexagonal 구조를 사용하면 쉽게 수정할 수 있고, 경계를 명확하게 구분지어 격리된다.

[이 부분은 도메인 주도 설계인 DDD의 내용과도 밀접한 관계를 가지고 있으므로, 다음 편에서 조금 더 설명한다.]

 

MSA구조를 선택하면서 중요시해야 할 점들은?


모든 구조는 시스템 성능을 위해 존재한다. 그리고 시스템 성능을 평가하는 지표는 Thoughput과 Latency로 설명할 수 있다.

 

  • Thoughput : 처리량을 측정한다. 단위 시간당 얼마나 많은 작업을 처리할 수 있는지를 나타낸다.
  • Latency : 지연시간을 의미한다. 특정 작업을 완료하기 위해 걸리는 시간을 나타낸다.

 

MSA에서는 각 서비스 별로 관리하기 때문에 확장이 쉽다. 따라서 Thoughput관점에서는 장점을 가진다고 할 수 있다.

반면, Latency관점에서는 서비스 간 통신, DB 액세스 같은 외부 통신에서 기존의 모놀리식 구조에 비해 약점을 가지고 있다.

 

그러므로 아키텍처 설계 과정에서 MSA를 선택하는 것이 무한정 좋은 것은 아니며, Thoughput과 Latency를 최적화하는 지점을 찾는 것이 중요하다.

 

 

728x90

들어가며,


 처음 자바를 공부한 지 어느새 1개월이 다가오는데, 기본 문법부터 시작해 지금은 MSA구조를 학습하고 있네요. 진도를 빠르게 나가다보니 기본기가 부족한걸 많이 느끼고 있던 터라 기본적인 Github 연동에 대해 포스팅하려고 합니다.

 

프로젝트를 하면 소스코드를 백업하는 용도로 많이 사용하게 되는데, 첫 프로젝트(?)를 정리해보려 합니다.

환경 설정


 

github에 로그인 후 내 프로필 setting으로 들어갑니다.

 

 

왼쪽 가장 밑에 보면 Developer settings가 있습니다.

 

classic 토큰을 생성합니다.

 

IDE의 setting에서 github검색한 뒤 +버튼으로 추가하면 된다.

github Repo 생성 후 업로드


깃허브 홈화면으로 돌아와 new버튼을 통해 Repo를 만든다.

 

 

명령어를 통해서도 만들 수 있고 push도 가능하다.

push로 업로드하는 방법은 3단계로 구성된다.

 

1. 특정 파일을 스테이징 영역에 추가

git add <file_name>

 

2. 스테이징 영역에 추가한 파일을 커밋

git commit -m "커밋 메시지"

 

3. 변경사항을 원격 저장소로 푸시

git push origin <branch_name>

'개발 환경' 카테고리의 다른 글

WAS 톰캣 설치부터 배포  (0) 2023.12.06
728x90

들어가며,


 

요즘 어플리케이션이 복잡해지면서 도커를 통해 컨테이너 단위 묶어 다루는 환경을 구성한다.

컨테이너는 쉽게 어디서든 어플리케이션을 실행할 수 있도록 패키징하는 기술이며, 이러한 컨테이너를 다루는 도구가 도커와 쿠버네티스다.

 

도커에서 생성한 컨테이너를 배포, 확장, 관리하기 위해 오케스트레이션 플랫폼을 이용하는데, 오픈 소스이면서 확장성이 좋아 K8s는 현재 표준이 되었다.

어플리케이션 배포 환경의 변화


 

전통적인 배포 시대

  • 물리적인 서버 위에 어플리케이션을 돌리다 보니 여러 어플리키이션의 리소스 한계를 겪음.

가상화된 배포 시대

  • 가상화 VM을 통해 VM간의 어플리케이션을 격리하여 하드웨어 자원을 효율적으로 활용.
  • OS위에 놓인 하이퍼바이저는 하나의 시스템 상에서 여러 개의 VM을 구동할 수 있도록 동작한다.
  • 다시 VM에는 OS, Bin/Library 등의 환경을 놓고 여러 개의 App을 구동한다.
  • 다만, VM역시 완전한 컴퓨터이기 때문에 OS를 설치해야 하며, 컨테이너 중심에 비해 무겁다는 단점.

컨테이너 개발 시대

  • Container Runtime을 통해 VM기반에서의 단점을 해결한다.
  • 별도의 OS를 설치할 필요없이 OS를 컨테이너 간의 공유 방식으로 동작하되, 전통 방식과 달리 컨테이너 별로 리소스 할당량을 나누었기 때문에 자원을 효율적으로 사용할 수 있게 된다.
  • 다만, OS에 문제가 발생하면 전체 컨테이너에 영향을 준다는 단점이 있다. 

 

 

쿠버네티스 클러스터의 구성


쿠버네티스 클러스터는 여러 노드를 가지고 있어 동시에 파드를 처리할 수 있다.

또한, 컨트롤 플레인에서 모든 Pod를 조율하여 각 노드에게 전달하므로 효율적으로 운영된다. 만약 하나의 노드에 문제가 생겨도 다른 노드가 역할을 대신 수행하여 계속해서 서비스를 제공할 수 있다.

컨프롤 플레인 

  • kube-apiserver : API를 노출하는 컴포넌트
  • etcd : 클러스터의 데이터를 담는 저장소
  • kube-scheduler : Pod를 감지하고 노드를 선택 
  • kube-controller-manager : 컨트롤러 프로세스를 실행하는 컴포넌트
    • 노드 컨트롤러: 노드가 다운되었을 때 통지와 대응에 관한 책임을 가진다.
    • 잡 컨트롤러: 일회성 작업을 나타내는 잡 오브젝트를 감시한 다음, 해당 작업을 완료할 때까지 동작하는 파드를 생성한다.
    • 엔드포인트슬라이스 컨트롤러: (서비스와 파드 사이의 연결고리를 제공하기 위해) 엔드포인트슬라이스(EndpointSlice) 오브젝트를 채운다
    • 서비스어카운트 컨트롤러: 새로운 네임스페이스에 대한 기본 서비스어카운트(ServiceAccount)를 생성한다.
  • cloud-controller-manager : 클라우드 별로 클러스터를 클라우드 API에 연결하고, (클라우드용, 클러스터용) 컴포넌트를 구분한다.

워커 노드 

  • kubelet : API서버와 통신하여 할당된 Pod에서 컨테이너가 확실하게 동작할 수 있도록 Pod를 실행하고 유지.
  • kube-proxy : 클러스터 내의 서비스와 파드의 네트워크 정보를 기록하고 이 정보를 참조해 통신을 돕는다.
  • 컨테이너 런타임 : 실제 컨테이너를 실행하는 도구 ex) 도
  •  

 

 

'Docker, K8S, AWS' 카테고리의 다른 글

Docker 도커란? 도커 이미지 사용 방법  (1) 2024.01.23
728x90

홈 컨트롤러 개발


@Controller 계층은 MVC패턴에서 클라이언트의 요청에 대응해 결과를 뷰에 전달하여, 웹 페이지를 생성한다.

@Slf4j와 log를 통해 로거 인스턴스를 생성한다. 

return "home"은 home.html 페이지를 반환함을 의미한다.

@Controller
@Slf4j
public class HomeController {

    @RequestMapping("/")
    public String home() {
        log.info("home controller");
        return "home";
    }

}

 

회원 컨트롤러 개발


@Controller
@RequiredArgsConstructor
public class MemberController {
 private final MemberService memberService;

 

/members/new로 들어오는 HTTP의 GET과 POST 메서드를 처리한다.

 

GET메서드가 들어올 때,

  • createForm은 모델 파라미터를 통해 뷰에 데이터를 전달하고 회원가입 폼을 생성하는 메서드다.
  • 모델 파라미터에는 "memberForm"이라는 이름의 빈 멤버 폼 객체가 뷰로 전달된다.
  • 뷰는 회원가입 폼 화면을 반환한다.

POST메서드가 들어오면,

  • @Valid를 통해 생성된 멤버 폼 객체의 유효성을 검증하고 BindingResult를 통해 결과를 확인한다.
  • 만약 결과가 에러를 가지고 있다면, members/createMemberForm 뷰로 이동한다.
  • 성공한다면, 홈 페이지로 redirect한다.
@GetMapping(value = "/members/new")
 public String createForm(Model model) {
 model.addAttribute("memberForm", new MemberForm());
 return "members/createMemberForm";
 }
 
 @PostMapping(value = "/members/new")
 public String create(@Valid MemberForm form, BindingResult result) {
 if (result.hasErrors()) {
 return "members/createMemberForm";
 }

 

members로 GET메서드가 들어오면,

멤버 리포지토리에서 findAll로 찾아온 값을 반환하는 멤버서비스의 findMembers메서드로 members라는 List를 생성한다.

모델 파라미터에 "members"라는 이름의 members값을 뷰로 전달한다.

@GetMapping(value = "/members")
 public String list(Model model) {
 List<Member> members = memberService.findMembers();
 model.addAttribute("members", members);
 return "members/memberList";
 }

 

주소 객체를 만드는데 form의 정보들을 변수로 불러와 생성한다.

새로운 멤버 정보를 빈 객체를 생성한 뒤, 폼에서 입력받은 이름 값과 생성된 주소값을 설정한다.

생성된 멤버 값은 멤버 서비스 계층에서 만든 회원가입 join 메서드를 활용해 멤버 서비스에 보내 DB에 반영시킨다.

화면은 redirect:/를 통해 홈화면으로 돌아간다.

Address address = new Address(form.getCity(), form.getStreet(), 
form.getZipcode());
 Member member = new Member();
 member.setName(form.getName());
 member.setAddress(address);
 memberService.join(member);
 return "redirect:/";
 }

 

 

728x90

주문 엔터티 개발


@Entity, @Table를 통해 엔터티 계층임을 지정하고 DB에서 매핑할 테이블명을 설정한다.

@Getter와 @Setter는 해당클래스의 조회와 설정을 허용함을 의미하는데, 엔터티의 값은 불변성을 유지해야 하므로 보통의 경우에는 Setter는 사용하지 않는다.

@Entity
@Table(name = "orders")
@Getter
public class Order {

주문 엔터티를 중심으로 보면, 연결된 엔터티간의 관계에서 아래와 같은 필드를 가진다.

  • 회원 id (FK) -> 한 명의 회원은 여러 주문 정보를 가질 수 있다. 

Member 엔터티와 다대일 양방향 관계를 설정한다. @JoinColumn은 member_id에 FK를 지정한다.

FetchType.LAZY를 사용해 지연로딩을 설정, 이는 실제 사용시 로딩됨을 의미한다.

  • 배송 id (FK) -> 하나의 주문은 하나의 배송 정보를 가진다.

Delivery 엔터티와 일대일 단방향 관계를 설정한다. 회원 id외 마찬가지로 delivery_id에 FK를 지정한다.

지연로딩 역시 마찬가지다. 그러나 CasacadeType을 설정해 생명주기를 관리한다는 점이 회원id와 차이가 있다.

이는 모든 변경 내용이 적용됨을 의미하는데, 주문과 배송의 관계에서 서로 반드시 필요한 존재이기 때문이다.

  • 주문 리스트 -> 하나의 주문은 여러 개의 주문 상품을 가질 수 있다.

연관관계를 맺을 때는 주인이 되는 엔터티를 결정해야 한다. 주인 엔터티가 DB에 직접적인 영향을 주는 주체이며, 주인이 아닌 엔터티는 단순 참조 역할을 한다.

주로 FK키 값을 지닌 쪽이 주인의 역할을 맡는다. 반대로 주인이 아닌 쪽은 mappedBy 속성을 통해 매핑된 필드를 지정한다.

private List<OrderItem> orderItems = new ArrayList<>();는 크기 조절에 동적인 자료구조를 통해 OrderItem 엔터티를 담는 컬렉션을 구현했다.

 

연관관계의 주인 측에 연관 관계 메서드를 설정한다.

불변성을 유지시키기 위해 필요한 메서드에 한하여 Setter를 사용할 수 있다.

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

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    //연관관계 메서드
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
    }

    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

 

주문 엔터티 자체에 속하며, 주문을 생성하는 정적인 메서드/

빈 주문 객체를 생성하고 회원 정보와, 배송 정보를 설정한다.

for문을 통해 주문 상품을 주문 리스트에 추가한다. 추가하는 동작은 연관관계 메서드에서 만든 addOrderItem을 사용한다.

주문 상태는 주문됨, 주문 일자는 현재 시각을 설정한다.

완성된 주문을 반환한다.

//생성 메서드
    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for (OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        order.setOrderStatus(OrderStatus.ORDER);
        order.setOrderdate(LocalDateTime.now());
        return order;
    }

 

주문을 취소하는 cancel메서드이다.

배송의 상태를 조회해서 배송상태가 COMP이면, IllegalStateException 예외 처리.

주문 객체의 주문상태를 취소됨으로 설정한다.

for문을 통해 주문에 속한 주문상품을 취소한다.

//비즈니스 로직
    //주문 취소
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) {
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }
        this.setOrderStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem : orderItems) {
            orderItem.cancel();
        }
    }

 

TotalPrice를 0으로 초기화한 뒤, for문을 통해 주문상품들을 돌며 주문상품의 전체 가격을 더한다.

//조회 로직
    //전체 주문 가격 조회
    public int getTotalPrice() {
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }

주문 상품 엔터티 개발


@Entity
@Table(name = "order_item")
@Getter
@Setter
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPrice;
    private int count;

 

주문에 속하는 주문상품을 생성하는 정적 메서드/

상품 객체, 주문가격, 수량을 변수로 받아 주문상품을 생성한다.

빈 주문상품 객체를 생성한 뒤, 상품, 주문가격, 수량을 설정한다.

-> 상품 엔터티의 재고는 수량만큼 감소시킨다.

생성된 주문상품 객체 반환.

    //생성 메서드
    public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);
        item.removeStock(count);
        return orderItem;
    }

createOrder와 createOrderItem의 관계 설명:

createOrderItem 메서드를 사용하여 주문 상품들을 생성하고, 이들을 createOrder 메서드에 전달하여 주문을 생성한다.

    //비즈니스 로직
    //주문 취소시 재고를 다시 증가시킴.
    public void cancel() {
        getItem().addStock(count);
    }

    //조회 로직
    //주문상품 전체 가격 조회 -> 주문가격과 수량을 조회해 곱셈.
    public int getTotalPrice() {
        return getOrderPrice()*getCount();
    }

 

주문 리포지토리 개발


리포지토리 개발 내용 참고.

 

회원 도메인 개발 Spring Boot 기본 (3)

들어가며 이전 1편과 2편에서의 내용은 Domain을 DB와 연동시키는 내용이었다. 3편부터 5편까지(아마도?)는 회원, 상품, 주문 도메인 내의 Controller, Service, Repository를 개발하여 기능을 부여하려고 한

wooltech.tistory.com

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public void save(Order order) {
        em.persist(order);
    }

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

 

주문 서비스 개발


@Service계층, 읽기 전용, 생성자 주입을 통해 틀을 갖춘다.

주문 서비스는 회원, 주문, 상품 리포지토리를 모두 주입받는다.

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

    private final MemberRepository memberRepository;
    private final OrderRepository orderRepository;
    private final ItemRepository itemRepository;

 

주문 서비스는 주문과 주문상품 엔터티에서 생성한 비즈니스 로직을 통해 기능 위임한다.

=> 도메인 모델 패턴을 통해 비즈니스 로직을 구현하는 메서드를 엔터티에서 사용하고 있기 때문에, 서비스 계층은 구현이 아닌 로직을 호출하고 트랜잭션을 관리하여, DB에 반영하는 역할을 수행한다.

=> 이후, 도메인 주도 설계 DDD에 대해 공부할 것.

 

회원id, 상품id, 수량을 변수로 받아 id값을 통해 리포지토리에서 반환했던 엔터티 값을 조회한다.

빈 배송정보 객체 생성한 뒤, 받아온 회원 엔터티의 주소 정보를 배송정보의 주소정보로 설정한다.

 

도메인에서 생성한 비즈니스 로직인 주문상품과 주문을 생성한다.

리포지토리에 구현된 주문 정보를 저장하고 id값을 반환한다.

 

//주문
    @Transactional
    public Long order(Long memberId, Long itemId, int count) {

        //엔티티 조회
        Member member = memberRepository.findOne(memberId);
        Item item = itemRepository.findOne(itemId);

        //배송정보 생성
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());

        //주문상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

        //주문 생성
        Order order = Order.createOrder(member, delivery, orderItem);

        //주문 저장
        orderRepository.save(order);
        return order.getId();
    }

 

728x90

상품 엔티티 개발


추상 클래스 (Abstract Class):

상품 클래스는 추상클래스로 설정되어 있다. 

추상클래스는 직접 객체 생성이 불가능하다. 상속을 통해 확장하고 자식(하위)클래스에서 구현된다.

 

상품 클래스가 가진 속성은 아래와 같다.

  • 상품 id (PK)
  • 이름
  • 가격
  • 재고수량
  • 카테고리 목록 

@Inheritance와 @DiscriminatorColumn은 상속 관계 매핑에서 단일 테이블 전략에 사용된다.

@Inheritance은 아래와 같이 전략 타입을 설정한다.

  • InheritanceType.SINGLE_TABLE: 단일 테이블 전략
  • InheritanceType.JOINED: 조인 전략
  • InheritanceType.TABLE_PER_CLASS: 구현 클래스마다 테이블 생성 전략

@DiscriminatorColumn을 dtype으로 명명해 하위 클래스의 값을 구분하여 다형성을 구현한다.

하위 클래스는 @DiscriminatorValue에 "값"을 지정해 단일 테이블 내에서 구분될 수 있다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_id")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

 

상품 엔티티는 stockQuantity라는 정수 값을 갖는다.재고 수량은 주문에 따라 변화하는 값이며, 0이하로 내려갈 수 없는 성격을 가지고 있다.따라서, 추가적인 비즈니스 로직이 필요하고 예외 처리를 위한 로직도 작성해야 한다.

 public void addStock(int quantity) {
 	this.stockQuantity += quantity;
 }
 public void removeStock(int quantity) {
 	int restStock = this.stockQuantity - quantity;
 	if (restStock < 0) {
 		throw new NotEnoughStockException("need more stock");
 	}
 	this.stockQuantity = restStock;
    }
}

 

add와 remove를 통해 증감을 계산하고, if문의 restStock이 0보다 작은 경우 예외처리를 수행한다.

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);
    }

    protected NotEnoughStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

비체크 예외인 RuntimeException을 상속 받아 생성자를 두고 다양한 예외에 대응한다.자바에서는 체크 예외와 비체크 예외를 통해 예외 처리를 한다. 이후 예외 처리에 대해 공부할 것.

 

상품 리포지토리 개발


 

@Repository
@RequiredArgsConstructor
public class ItemRepository {
	private final EntityManager em;

	public void save(Item item) {
		if (item.getId() == null) {
			em.persist(item);
		} else {
			em.merge(item);
 		}
	}

 

 

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();
 }

 

상품 서비스개발


회원 서비스 개발 내용 참고.

 

회원 도메인 개발 Spring Boot 기본 (3)

들어가며 이전 1편과 2편에서의 내용은 Domain을 DB와 연동시키는 내용이었다. 3편부터 5편까지(아마도?)는 회원, 상품, 주문 도메인 내의 Controller, Service, Repository를 개발하여 기능을 부여하려고 한

wooltech.tistory.com

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
    //lombok 생성자 주입
    private final ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item) {
        itemRepository.save(item);
    }

    public List<Item> findItems() {
        return itemRepository.findAll();
    }

    public Item findOne(Long itemId) {
        return itemRepository.findOne(itemId);
    }

 

728x90

들어가며


 

 

 

MVC패턴


2023.11.26 - [Learning Java Spring] - 처음 자바 스프링을 공부한다면?Java Spring 프레임워크의 기본

 

처음 자바 스프링을 공부한다면?Java Spring 프레임워크의 기본

백엔드 개발자를 선택하고 나서 개발자에게 프론트는 js, 백은 Java를 선택해야(?)만 할정도로 한국 사회에서 가장 널리 보급되고 있는 것 같다. 프론트 백엔드 웹 html, css, js (React, Next) Java, Java Spri

wooltech.tistory.com

회원 엔터티 개발


@Entity @Getter @Setter 등을 통해 클래스 생성하며, @Id @GeneratedValue @Column으로 기본키 PK설정 및 컬럼명을 지정한다.

 

 

@Entity
@Table(name = "member")
@Getter @Setter
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

@Embeded와 protected를 통해 별도의 정보 클래스를 정의하고 상위 엔티티에 포함시키는 방법:

  • 회원과 배송 모두 @Embeded를 통해 주소 클래스의 정보를 가진다.
  • 주소 클래스는 생성자를 통해 객체 정보를 사용가능하도록 초기화 한다.
  • 생성된 객체는 protected로 보호되는데 이는 캡슐화된 정보에 접근하기 위한 접근자이다. 즉, 변경을 제한한다.
@Embedded
 private Address address;

@Embeddable
@Getter
public class Address {
 private String city;
 private String street;
 private String zipcode;
 
 protected Address() {
 }
 public Address(String city, String street, String zipcode) {
 this.city = city;
 this.street = street;
 this.zipcode = zipcode;
 }

 

 

 

회원 리포지토리 개발


@Repository를 통해 리포지토리 클래스임을 지정한다.

@PersistenceContext에서 엔티티 매니저를 주입해 도메인에서 만들어 둔 엔티티를 관리한다.

 

save(), findOne(), findAll(), findByName() 메서드를 통해 본격적으로 기능을 구현한다.

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

 

1.  save(Member member) 메서드:

save를 통해 전달 받은 Member 객체를 persist메서드를 사용하여 영속성 컨텍스트에 저장한다.

이후 서비스 계층에서 트랜잭션이 커밋되면, DB에 반영된다.

 

2.  findOne(Long id) 메서드:

Member의 주어진 id를 찾는 메서드이다.

1차적으로 영속성 컨텍스트내의 캐시에서 조회를 한 후, 없다면 JPA가 직접 데이터베이스에 select구문을 만들어 조회한다. 이는 id가 식별자이기 때문에 가능한 것이다.

  • em.find(클래스, 기본키)

3. findAll() 메서드:

DB에 저장된 모든 회원을 List로 반환한다. 

select m from Member m을 통해 모든 회원을 조회하는 쿼리 구문을 생성한다.

select 쿼리가 실행된 후 getResultList를 통해 리스트로 받아온다.

 

4. findByName(String name) 메서드:

매개변수인 이름값으로 조회하는 방법

이름에 들어갈 내용은 동적인 데이터이기 때문에 setParameter 매개변수 바인딩을 사용해 안정적으로 제공한다.

예를 들면,

select m from Member m where m.name = 'John' 이 쿼리에서는 .setParameter("name", John)이 된다.

    public void save(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", 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();
    }
}

회원 서비스 개발


@Service 서비스 계층임을 나타내며, readOnly = true를 통해 읽기 전용으로 사용된다.

즉, 데이터의 수정은 일어나지 않음을 의미한다.

@RequiredArgsConstructor를 통해 생성자를 자동으로 생성하여, ItemRepository를 주입받는다.

(final 또는 @NonNull로 표시된 필드를 사용하여, 생성자를 만든다.)

 

생성자 주입을 하는 이유:

  • 리포지토리에서 구현된 회원 정보를 외부에서 받아와 사용하기 때문에 유연성과 재사용성의 장점을 가진다.
  • 리포지토리의 회원 정보를 수정하더라도 서비스의 내용은 수정하지 않아도 된다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

 

메서드에서 id를 반환하는 이유:

  • 새로운 회원정보가 데이터베이스에 저장되고 회원id가 반환되어야 클라이언트가 이 정보를 활용할 수 있다.
  • 로그인, 회원 정보 수정 등에 활용된다.
 //회원가입
    @Transactional
    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

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

 

 회원조회는 사용하지 않고 회원가입에만 Transactional이 사용된 이유:

  • Transactional을 사용하는 이유는 트랜잭션을 통해 리포지토리에서 구현한 정보를 DB에 반영하기 위함이다.
  • 즉, 가입에는 새로운 회원 정보를 DB에 저장해야 하지만 회원 조회에서는 새로운 값을 넣는 것이 아닌 get을 통한 호출만 다루고 있기 때문에 Transactional이 필요하지 않다.
    // 전체 회원 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

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

+ Recent posts