본문 바로가기
Backend/공부 일지

2024/04/01(월)

by 박상윤 2024. 4. 1.

벌써 4월 시작

매일 꾸준하게 하자 화이팅

공부하게 하시는 하나님께 감사드린다.

 

Part. CS 

캐시

데이터를 미리 복사해 놓는 임시 저장소이자 빠른 장치와 느린 장치에서 속도  차이에 따른 병목 현상을 줄이기 위한 메모리

ex) 실제로 메모리와 CPU 사이의 속도 차이가 너무 크기 때문에 그 중간에 레지스터 계층을 둬서 속도 차이를 해결한다.

 

속도 차이를 해결하기 위해 계층과 계층 사이에 있는 계층을 캐싱 계층이라고 한다.

ex) 캐시 메모리와 보조기억장치 사이에 있는 주기억장치를 보조기억장치의 캐싱 계층이라고 한다.

 

지역성의 원리

캐시 계층을 두는 것 말고 캐시를 직접 설정할 때는 어떻게 해야 할까?

자주 사용하는 데이터를 기반으로 설정해야 한다. 자주 사용하는 데이터에 대한 근거가 되는 것은 무엇인가? 지역성!

지역성 : 시간 지역성, 공간 지역성으로 나뉜다.

 

시간 지역성

최근 사용한 데이터에 다시 접근하려는 특성을 말한다.

ex) 반복문에서 변수 i

 

공간 지역성

최근 접근한 데이터를 이루고 있는 공간이나 그 가까운 공간에 접근하는 특성

ex) 변수 i의 값을 담고 있는 배열

int[] arr = new int[10];

for(int i = 0; i < 10; i++){
	arr[i] = i; // i : 시간 지역성
}

// 배열 arr : 공간 지역성

 

캐시히트와 캐시미스

캐시 히트 : 캐시에서 원하는 데이터를 찾은 경우

캐시 미스 : 해당 데이터가 캐시에 없는 경우, 메모리로 가서 데이터를 찾아오는 것

 

캐시 히트를 하게 되는 경우 해당 데이터를 제어장치를 거쳐 가져오게 된다.

캐시 히트는 위치도 가깝고 CPU 내부 버스를 기반으로 작동하기 때문에 빠르다.

캐시 미스가 발생되면 메모리에서 가져오게 되는데, 이는 시스템 버스 기반으로 작동하므로 느리다.

 

캐시 매핑

캐시가 히트되기 위해 매핑하는 방법을 말하며, CPU 레지스터와 주 메모리 간(RAM)에 데이터를 주고받을 때를 기반으로 설명한다.

레지스터는 주 메모리에 비하면 굉장히 작고 주 메모리는 굉장히 크기 때문에 작은 레지스터가 캐시 계층으로써 역할을 잘 해주려면 이 매핑을 어떻게 하느냐가 중요하다.

 

이름 설명
직접 매핑 메모리가 1~100 존재, 캐시가 1~10이 존재하는 경우, 1:1~10, 2:1~20과 같은 방식으로 매핑하는 것을 말한다.
연관 매핑 순서를 일치시키지 않고 관련 있는 캐시와 메모리를 매핑한다. 충돌이 적지만 모든 블록을 탐색해야 해서 속도가 느리다.
집합 연관 매핑 직접 매핑과 연관 매핑을 합쳐 놓은 것이다. 순서는 일치시키지만 집합을 둬서 정하며 블록화 되어 있기 때문에 검색은 좀 더 효율적이다.
ex) 1~100이 있고, 캐시가 1~10이 있다면 캐시 1~5에는 1~50의 데이터를 무작위로 저장시키는 것을 말한다.

 

웹 브라우저의 캐시

소프트웨어의 대표적인 캐시 : 쿠키, 로컬 스토리지, 세션 스토리지

사용자의 커스텀한 정보나 인증 모듈 관련 사항들을 웹 브라우저에 저장해서 추후 서버에 요청할 때 자신을 나타내는 아이덴티티나 중복 요청 방지를 위해 쓰인다.

 

쿠키

만료기한이 있는 키-값 저장소(최대 4KB)

클라이언트 또는 서버에서 만료기한 등을 정할 수 있는데 보통 서버에서 만료기한을 정한다.

 

로컬 스토리지

만료기한이 없는 키-값 저장소(최대 10MB)

웹 브라우저를 닫아도 유지되고 도메인 단위로 저장, 생성 된다.

HTML5를 지원하지 않는 웹 브라우저에서는 사용할 수 없다.

 

세션 스토리지

만료기한이 없는 키-값 저장소(최대 5MB)

탭 단위로 세션 스토리지를 생성하며, 탭을 닫을 때 해당 데이터가 삭제된다.

HTML5를 지원하지 않는 웹 브라우저에사는 사용할 수없다.

 

데이터베이스의 캐싱 계층

메인 데이터베이스 위에 레디스(redis) 데이터베이스 계층을 캐싱 계층으로 둬서 성능을 향상시키기도 한다.

캐시 히트 : 레디스로부터 데이터를 읽어온다.

캐시 미스 : 메인 데이터베이스로부터 데이터를 가져온다.

 

메모리 관리

운영체제의 대표적인 할 일 중 하나

컴퓨터 내의 한정된 메모리를 극한으로 활용해야 하는 것

 

가상 메모리

메모리 관리 기법의 하나로 컴퓨터가 실제로 이용 가능한 메모리 자원을 추상화하여 이를 사용하는 사용자들에게 매우 큰 메모리로 보이게 만드는 것

 

가상적으로 주어진 주소 : 가상주소

실제 메모리상에 있는 주소 : 실제 주소

 

가상주소는 메모리관리장치(MMU)에 의해 실제 주소로 변환되므로,

사요자는 실제 주소를 의식할 필요 없이 프로그램을 구축하게 된다.

 

가상 메모리는 가상 주소와 실제 주소가 매핑되어 있고 프로세스의 주소 정보가 들어 있는 페이지 테이블로 관리한다.

속도 향상을 위해 TLB를 사용한다.

 

TLB
메모리와 CPU 사이에 있는 주소 변환을 위한 캐시
페이지 테이블에 있는 리스트를 보관하며 CPU가 페이지 테이블까지 가지 않도록 해 속도를 향상시킬 수 있는 캐시 계층

 

스와핑

가상 메모리는 존재하지만 실제 메모리인 RAM에는 현재 없는 데이터나 코드에 접근할 경우 페이지 폴트가 발생한다.

메모리에서 당장 사용하지 않는 영역을 하드디스크로 옮기고 하드디스크의 일부분을 마치 메모리처럼 불러와 쓰는 것을 스와핑이라고 한다.

페이지 폴트가 발생하지 않은 것처럼 만든다.

 

+ 주기억장치에 적재한 하나의 프로세스를 보조기억 장치에 잠시 적재했다가 필요할 때 다시 꺼내서 사용하는 메모리 교체 기법


https://resilient-923.tistory.com/397

 

[운영체제(OS)] 스와핑(swapping), 가상메모리(virtual memory) 란?

저번 시간에는 페이징과 세그멘테이션 방법에 대해서 살펴봤습니다. 모두 메모리를 어떻게 효율적으로 쓸 것이냐에 대한 고민을 바탕으로 나온 방법들이었는데요. 그렇다면 페이징 + 세그멘테

resilient-923.tistory.com

 

페이지 폴트

프로세스의 주소 공간에는 존재하지만 지금 컴퓨터의 RAM에는 없는 데이터에 접근했을 경우에 발생한다.

페이지 폴트와 그로 인한 스와핑 과정

(1) CPU는 물리 메모리를 확인하여 해당 페이지가 없으면 트랩을 발생해서 운영체제에 알린다.

(2) 운영체제는 CPU의 동작을 잠시 멈춘다.

(3) 운영체제는 페이지 테이블을 확인하여 가상 메모리에 페이지가 존재하는지 확인하고, 없으면 프로세스를 중단하고 현재 물리 메모리에 비어 있는 프레임이 있는지 찾는다. 물리 메모리에도 없다면 스와핑이 발동된다.

(4) 비어 있는 프레임에 해당 페이지를 로드하고, 페이지 테이블을 최신화한다.

(5) 중단되었던 CPU를 다시 시작한다.

 

페이지(page)
가상 메모리를 사용하는 최소 크기 단위

프레임(frame)
실제 메모리를 사용하는 최소 크기 단위

 

Part. Spring JPA

김영한의 JPA 책을 공부해보자

07. 고급 매핑

- 상속 관계 매핑 : 객체의 상속 관계를 데이터베이스에 어떻게 매핑하는지 다룬다.

- @MappedSuperclass: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받고 싶으면 이 기능을 사용하면 된다.

- 복합 키와 식별 관계 매핑 : 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법을 다룬다.

- 조인 테이블 : 테이블은 외래 키 하나로 연관관계를 맺을 수 있지만 연관관계를 관리하는 연결 테이블을 두는 방법도 있다.

- 엔티티 하나에 여러 테이블 매핑하기 : 엔티티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 있다.

 

상속 관계 매핑

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.

대신에 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때 선택하는 3가지 방법

(1) 각각의 테이블로 변환 : 각각을 모두 테이블로 만들어 조회할때 조인을 사용한다. (JPA에선 조인 전략이라 한다.)

(2) 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합한다.

(3) 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다. ( JPA에선 구현 클래스마다 테이블 전략이라 한다.)

 

조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략

조회할때 조인을 자주 사용한다. 주의할 점으로는 객체는 타입 구분이 가능하지만, 테이블은 타입의 개념이 존재하지 않는다.

타입을 구분하는 컬럼(DTYPE - 구분 컬럼)을 추가해야 한다.

 

Item class

package com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Item;

import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name; // 이름
    private int price; // 가격
}

 

Album class

package com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Album;

import com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Item.Item;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

 

Movie class

package com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Movie;

import com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Item.Item;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director; // 감독
    private String actor; // 배우
}

 

매핑 정보 분석

(1) @Inheritance(strategy = InheritanceType.JOINED) : 상속 매핑은 부모 클래스에 @Inheritance를 사용해야 한다.

매핑 전략을 지정해야 하는데 조인 전략을 사용하므로 InheritanceType.JOINED를 사용

 

(2) @DiscriminatorColumn(name = "DTYPE") : 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 지정된 자식 테이블을 구분할 수 있다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 된다.

 

(3) @DiscriminatorValue("M") : 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.

영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 값 M이 저장된다.

 

자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 자식 테이블의 기본 키 컬럼명을 변경하고 싶은 경우엔 @PrimaryKeyJoinColumn을 사용하면 된다.

package com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Book;

import com.example.spring_jpa_basic.chatper7.JoinColumn_startegy.entity.Item.Item;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.PrimaryKeyJoinColumn;

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
    private String author; // 작가
    private String isbn; // ISBN
}

 

장점

- 테이블 정규화

- 외래 키 참조 무결설 제약조건을 활용

- 저장공간을 효율적으로 활용

 

단점

- 조회할 때 조인이 많이 사용되므로 성능이 저하된다.

- 조회 쿼리가 복잡하다.

- 데이터를 등록할 INSERT SQL을 두 번 실행한다.

 

특징

- 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn) 없이도 동작한다.

 

관련 어노테이션

- @PrimaryKeyJoinColumn, @DiscriminatorColumn, @DiscriminatorValue

 

 

정규화

https://mangkyu.tistory.com/110

 

[Database] 정규화(Normalization) 쉽게 이해하기

지난 포스팅에서 데이터베이스 정규화와 관련된 내용을 정리했었다. 하지만 해당 내용이 쉽게 이해되지 않는 것 같아서 정규화 관련 글을 풀어서 다시 한번 정리해보고자 한다. 1. 정규화(Normaliz

mangkyu.tistory.com

외래 키 참조 무결성 제약 조건

릴레이션은 참조할 수 없는 외래 키 값을 가져서는 안 된다는 것

 

단일 테이블 전략

이름 그대로 테이블을 하나만 사용하는 전략이다.

구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.

조회시 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

이 전략을 사용시 주의할 점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다.

ex) Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용하고 다른 엔티티와 매핑된 ARTIST, DIRECTOR, ACTOR 컬럼은 사용하지 않으므로 null이 입력되기 때문이다.

 

JPA 활용 2편

 

JPA에서 DTO로 바로 조회하기

JPA에서 DTO로 바로 조회를 하게되면, 리포지토리의 재사용성이 떨어지고 API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점이 존재한다.

 

이 단점을 상쇄할 수 있는 방법이 존재한다.

어떻게 하면 될까?

repository나 하위에 새로운 패키지 계층을 만들어준다.

Repository는 가급적 순수한 Entity를 조회하는데에만 사용해야 한다.

분리를 하는 것을 개인적으로 권장한다.(김영한)

 

 

API 개발 고급 - 컬렉션 조회 최적화

 

주문 조회 v1: 엔티티 직접 노출

 

OneToMany 일대다 관계를 조회하고, 최적화하는 방법에 대해서 알아보자!

 

버전별 API 스펙

V1. 엔티티 직접 노출
- 엔티티가 변하면 API 스펙이 변한다.
- 트랜잭션 안에서 지연 로딩 필요
- 양방향 연관관계 문제

 

V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용 x)
- 트랜잭션 안에서 지연 로딩 필요

 

V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용 O)
- 페이징 시에는 N 부분을 포기해야 함(대신에 batch fetch size? 옵션 주면 N -> 1 쿼리로 변경 가능)

 

V4. JPA에서 DTO로 바로 조회, 컬렉션 N 조회 ( 1 + N Query)
- 페이징 가능

 

V5. JPA에서 DTO로 바로 조회, 컬렉션 N 조회 ( 1 + 1 Query)
- 페이징 가능

 

V6. JPA에서 DTO로 바로 조회, 플랫 데이터(1Query) (1 Query)
- 페이징 불가능

 

 

주문 조회 V1

@RestController
@RequiredArgsConstructor
public class OrderApiController {
	private final OrderRepository orderRepository;
    
    // 엔티티 직접 노출
    // Hibernate5Module 등록, LAZY=null 처리
    // 양방향 관계 문제 발생 -> @JsonIgnore
    
    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1(){
    	List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for(Order order: all){
        	order.getMember().getName(); // Lazy 강제 초기화
            order.getDelivery.getAddress(); // Lazy 강제 초기화
            List<OrderItem> orderItems = order.getOrderItems();
            OrderItems.stream().forEach(o -> o.getItem().getName()); // Lazy 강제 초기화
        }
        
        return all;
    }
}

- orderItem, item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.

- 양방향 연관관계면 무한 루프에 걸리지 않게 한 곳에 @JsonIgnore를 추가해야 한다.

- 엔티티를 직접 노출하므로 좋은 방법은 아니다.

 

주문 조회 V2 : 엔티티 DTO로 변환

@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
	List<Order> orders = orderRepository.findAllByString(new OrderSearch());
   	List<OrderDto> result = orders.stream()
    	.map(o ->. new orderDto(o))
        .collect(toList();
    
    return result;
}

 

@Data
statc class OrderDto {
	private Long orderId;
    private String name;
    private LocalDateTime orderDate; // 주문시간
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemDto> orderItems;
    
    public OrderDto(Order order){
    	orderId = order.getId();
        name = order.getMember().getName();
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
        orderItems = order.getOrderItems().stream()
        		.map(orderItem -> new OrderItemDto(orderItem))
                .collect(toList());
    }
}

@Data
static class orderItemDto{
	private String itemName; // 상품 명
    private int orderPrice; // 주문 가격
    private int count; // 주문 수량
    
    public OrderItemDto(OrderItem orderItem){
    	itemName = orderItem.getItem().getName();
        orderPrice = orderItem.getOrderPrice();
        count = orderItem.getCount();
    }
}

지연 로딩으로 인한 너무 많은 SQL 실행

SQL 실행 수

- order 1번

- member, address N번(order 조회 수 만큼)

- orderItem N번(order 조회 수 만큼)

- item N번(orderItem 조회 수 만큼)

지연 로딩은 영속성 컨텍스트에 있으면 영속성 컨텍스트에 있는 엔티티를 사용하고 없으면 SQL을 실행한다.
따라서 같은 영속성 컨텍스트에서 이미 로딩한 회원 엔티티를 추가로 조회하면 SQL을 실행하지 않는다.

 

주문 조회 V3 : 엔티티 DTO로 변환 - 페치 조인 최적화

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
	List<Order> orders = orderRepository.findAllwithItem(0;
    List<OrderDto> result = orders.stream()
    	.map(o -> new OrderDto(o))
        .collect(toList());
    
    return result;
}

 

OrderRepository에 추가

public List<Order> findAllWithItem(){
	return em.createQuery(
    	"select distinct o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d" +
        " join fetch o.orderItem oi" +
        " join fetch oi.item i", Order.class)
        .getResultList();
    )
}

 

장점

페치 조인으로 SQL이 1번만 실행된다.

distinct를 사용한 이유는 1대다 조인이 있으므로 데이터베이스 row가 증가한다. 그 결과 같은 order 엔티티의 조회 수도 증가하게 된다.

JPA의 distinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면, 애플리케이션이 중복을 걸러준다. order가 컬렉션 페치 조인 때문에 중복 조회 되는 것을 막아준다.

 

단점

페이징이 불가능하다.

 

참고
컬렉션 페치 조인을 사용하면 페이징이 불가능하다. 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다.

컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정합하게 조회될 수 있다.

 

주문 조회 V3 : 엔티티 DTO로 변환 - 페이징과 한계 돌파

컬렉션을 페치 조인하면 페이징이 불가능하다.

- 컬렉션을 패치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.

- 일대다에서 일(1)을 기준으로 페이징을 하는 것이 목적인다. 그러나 데이터는 다(N)를 기준으로 row가 생성된다.

- 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애로 이어질 수 있다.

 

그럼 페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까?

 

대부분의 페이징 + 컬렉션 엔티티 조회 문제는 다음과 같은 방법으로 해결이 가능하다.

- ToOne(OneToOne, ManyToOne)관계를 모두 페치조인 한다. ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.

- 컬렉션은 지연 로딩으로 조회한다.

- 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BatchSize를 적용한다.

  - hibernate.default_batch_fetch_size : 글로벌 설정

  - @BatchSize: 개별 최적화

  이 옵션 사용시 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다.

 

OrderRepository

public List<Order> findAllWithMemberDelivery(int offset, int limit){
	return em.createQuery(
    	"select o from Order o" +
        	" join fetch o.member m" +
            " join fetch o.delivery d" , Order.class)
     	.setFirstResult(offset)
        .setMaxResults(limit)
        .getResultList();
    )
}

 

OrderApiController

// 엔티티를 조회해서 DTO로 변환 페이징 고려
// ToOne 관계만 우선 모두 페치 조인으로 최적화
// 컬렉션 관계는 hiberante.default_batch_fetch_size, @BatchSize로 최적화

@GetMapping("/api/v3.1/orders")
public List<orderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
									@RequestParam(value = "limit", defaultValue = "100) int limit){
                                    
                                    	List<Order> orders = orderRepository.findAllWithMemberDeliverY(offset,limit);
                                        List<OrderDto> result = orders.stream()
                                        	.map(o -> new OrderDto(o))
                                            .collect(toList());
                                    
                                    	return result;
                                    }

 

최적화 옵션

spring:
	jpa:
    	properties:
        	hibernate:
            	default_batch_fetch_size: 1000

개별로 설정하려면 @BatchSize를 적용하면 된다.(컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)

 

https://bcp0109.tistory.com/333

 

JPA Batch Size

1. Overview BatchSize 는 JPA 의 성능 개선을 위한 옵션 중 하나입니다. 여러 개의 프록시 객체를 조회할 때 WHERE 절이 같은 여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줍니다. 간단한 테스트와

bcp0109.tistory.com

 

졸업작품 개발 시작!

 

토비의 Spring을 공부해보자

 

높은 응집도와 낮은 결합도

높은 응집도 : 변화가 일어날 때 해당 모듈에서 변하는 부분이 크다는 것

변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다.

 

낮은 결합도 : 책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 즉 느슨하게 연결된 형태를 유지하는 것

느슨한 연결은 관계를 유지하는데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것, 결합도가 낮아지면 변화에 대응하는 속도가 높아지고 구성이 깔끔해진다.

ex) ConnectionMaker 인터페이스의 도입으로 인해 DB 연결 기능을 구현한 클래스가 바뀌더라도 DAO의 코드는 변경될 필요가 없게 됨

 

제어의 역전(IoC)

 

UserDao가 ConnectionMaker 인터페이스를 구현한 특정 클래스로부터 완벽하게 독립할 수 있도록 UserDao의 클라이언트인 UserDaoTest가 그 수고를 담당하게 된 상황이다. UserDaoTest 역할은 UserDao 기능이 잘 동작하는지를 테스트하려고 만든 것인데,

또 다른 책임까지 떠맡고  있으니 뭔가 문제가 있는 상황이다.

 

어떻게? 성격이 다른 책임이나 관심사는 분리해버린다.

UserDao와 ConnectionMaker 구현 클래스의 오브젝트를 만드는 것과 그렇게 만들어진 두 개의 오브젝트가 연결돼서 사용될 수 있도록 관계를 맺어주는 것

 

 

팩토리

이 클래스의 역할을 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 것이다.

이러한 일을 하는 오브젝트를 흔히 팩토리라고 부른다.(추상 팩토리 패턴이나 팩토리 메소드 패턴과는 다르다.)

package com.example.tobispring.오브젝트와의존관계.dao;

public class DaoFactory {
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new NConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}

 

DaoFactory의 UserDao 메소드를 호출하면 DConnectionMaker를 사용해 DB 커넥션을 가져오도록 이미 설정된 UserDao 오브젝트를 돌려준다.

 

UserDaoTest는 이제 UserDao가 어떻게 만들어지는지 어떻게 초기화되어 있는지에 신경 쓰지 않고 팩토리로부터 UserDao 오브젝트를 받아다가, 자신의 관심사인 테스트를 위해 활용하기만 하면 그만이다.

 

Test코드

package com.example.tobispring.오브젝트와의존관계;

import com.example.tobispring.오브젝트와의존관계.dao.DaoFactory;
import com.example.tobispring.오브젝트와의존관계.dao.UserDao;

public class UserDaoTest {
    public static void main(String[] args) {
        UserDao userDao = new DaoFactory().userDao();
    }
}

 

오브젝트 팩토리의 활용

여러 개의 DAO가 더 많아지면 ConnectionMaker의 구현 클래스를 바꿀 때마다 모든 메소드를 일일이 수정해야한다.

package com.example.tobispring.오브젝트와의존관계;

import com.example.tobispring.오브젝트와의존관계.dao.AccountDao;
import com.example.tobispring.오브젝트와의존관계.dao.UserDao;
import com.example.tobispring.오브젝트와의존관계.dao.messageDao;

public class DaoFactory {
    
    public UserDao userDao(){
        ConnectionMaker connectionMaker = new NConnectionMaker();
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
    
    public AccountDao accountDao(){
        return new AccountDao(new NConnectionMaker());
    }

    public messageDao messageDao(){
        return new messageDao(new NConnectionMaker());
    }
}

 

중복 문제를 해결하기 위해서 가장 좋은 방법은 분리해내는 것이다.

ConnectionMaker의 구현 클래스를 결정하고 오브젝트르 만드는 코드를 별도의 메소드로 뽑아내자!

package com.example.tobispring.오브젝트와의존관계;

import com.example.tobispring.오브젝트와의존관계.dao.AccountDao;
import com.example.tobispring.오브젝트와의존관계.dao.UserDao;
import com.example.tobispring.오브젝트와의존관계.dao.messageDao;

public class DaoFactory {

    public UserDao userDao(){
        return new UserDao(connectionMaker());
    }

    public AccountDao accountDao(){
        return new AccountDao(connectionMaker());
    }

    public messageDao messageDao(){
        return new messageDao(connectionMaker());
    }
    
    private ConnectionMaker connectionMaker(){
        return new NConnectionMaker();
    }
}

 

제어권 이전을 통한 제어관계 역전

제어의 역전이라는 것은 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다.

제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다.

 

ex) 서블릿

서블릿 안에 main() 메소드가 있어서 직접 실행시킬 수 있는 것도 아니다. 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메소드를 호출한다.

 

 

알고리즘 빼고 다했다..

알고리즘도 같이 가져가자..!

 

'Backend > 공부 일지' 카테고리의 다른 글

2024/04/03(수)  (1) 2024.04.04
2024/04/02(화)  (0) 2024.04.03
2024/03/31(일)  (1) 2024.04.01
2024/03/30(토)  (0) 2024.03.31
2024/03/29(금)  (1) 2024.03.30