728x90

https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/description/

 

주어진 n의 값을 연속적으로 이진법으로 만들고 

그 값을 다시 10진법으로 만들어 구하는 문제

 

 

최종 코드 

n이 12일때

package 리트코드;

public class ConcatenationOfConsecutiveBinaryNumbers1680 {
    private static final int MOD = 1000000007;

    public int concatenatedBinary(int n) {
        //long은 19자리수 가능
        long result = 0;

        for (int i = 1; i <= n; i++) {
            int num = i;
            StringBuilder binaryStr = new StringBuilder();
            while (num > 0) {
                binaryStr.append(num % 2); //나머지 넣기
                num /= 2; //2나누기

            }

            binaryStr.reverse();
            String binary = binaryStr.toString();

            //비트 연산을 통해 이진 문자열을 추가
            for (char c : binary.toCharArray()) {
                //2로 곱하고 나머지를 더하는 식임
                // '0'을 빼면 수로 변환됨
                //MOD로 나눈 나머지로 구하는 이유는 매우큰 숫자 다룰 때 오버플로우 방지하고 결과를 적절한 범위내에서 유지하기 위함임
                result = (result * 2 + (c - '0')) % MOD;
                System.out.println("result = " + result);

            }


        }
        return (int) result;

    }

    public static void main(String[] args) {
        int n = 12;
        ConcatenationOfConsecutiveBinaryNumbers1680 concatenationOfConsecutiveBinaryNumbers1680 = new ConcatenationOfConsecutiveBinaryNumbers1680();
        int result = concatenationOfConsecutiveBinaryNumbers1680.concatenatedBinary(n);
        System.out.println("result = " + result);

    }
}

 

result = 1
result = 3
result = 6
result = 13
result = 27
result = 55
result = 110
result = 220
result = 441
result = 882
result = 1765
result = 3531
result = 7063
result = 14126
result = 28253
result = 56507
result = 113015
result = 226031
result = 452062
result = 904124
result = 1808248
result = 3616497
result = 7232994
result = 14465988
result = 28931977
result = 57863955
result = 115727910
result = 231455821
result = 462911642
result = 925823285  -> 여기까진 값네 925823285 
result = 851646563  (-> 나누지 않은 값이 1851646570 = 925823285 *2  로, 10자리여서 MOD로 나눠지게 됨. 그 나머지값임)
result = 703293120
result = 406586234
result = 813172469
result = 626344932
result = 252689857
result = 505379714
result = 505379714

 

근데 만약 MOD를 나눈 나머지 값으로 구하지 않는다면?
 private static final int MOD = 1000000007;

result = (result * 2 + (c - '0'));

 

 

result = 1
result = 3
result = 6
result = 13
result = 27
result = 55
result = 110
result = 220
result = 441
result = 882
result = 1765
result = 3531
result = 7063
result = 14126
result = 28253
result = 56507
result = 113015
result = 226031
result = 452062
result = 904124
result = 1808248
result = 3616497
result = 7232994
result = 14465988
result = 28931977
result = 57863955
result = 115727910
result = 231455821
result = 462911642
result = 925823285  -> 9자리 

--여기까지 같음-- 
result = 1851646570 = 925823285 *2  

result = 3703293141  =  1851646570 * 2 +(나머지 1 더함 )

result = 7406586283
result = 14813172567
result = 29626345135
result = 59252690270
result = 118505380540 
result = -1753703748 = 118505380540 * 2 = 237,010,761,080

 

근데 long이 19자리수까지 가능한데도 굳이 MOD를 나누는 이유는 

수의 크기가 너무 커져서 오버플로우가 일어나는 것을 방지하기 위함.

MOD 상수값을 나누고 그 나머지로 사용한다는

 

문제의 조건이었음!!

 

 

728x90

'코딩테스트 > 릿코드' 카테고리의 다른 글

릿코드 875. Koko Eating Bananas 바이너리 서치  (0) 2023.08.31
728x90

Colima를 통한 도커 환경과 

도터 데스크탑 환경 이렇게 두가지 환경을 구축해놓음

mysql 계정 설정했던 환경은 도커 데스크탑 환경이었어서 colima를 통해 띄운 도커 컨테이너로는 접속이 안되었으나 

이를 모르고 잠시 헤맨 이야기

 

 

발단

..시작은 mysql 실행을 위한 설치 재설치 실행 실행실패를 하다보니 도커를 통해서 mysql을 설치했다.

전개

도커를 통해 mysql 접속도 성공함! 이미지 선택해서 컨테이너 실행해서 비밀번호도 만들고 들어가서 데이터베이스도 만들었다! 

위기

근데 도커데스크탑 중지하고 연결을 끊고나서 도커 cli를 통해 들어가려는데 계속 막히는 것.

어떻게 막혔냐.

먼저 도커 cli 접속안됨

❯ docker ps Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

그래서 colima를 켜서 접속함

절정

근데! 

docker exec -it my-mysql-container mysql -uroot -p

접속이 안됨

 

실행되는 컨테이너 ID를 확인해보니 docker desktop의 설정파일에서 본 컨테이너 ID와 다름

 

결말

colima를 껐을때랑 켰을 때랑 다른 도커가 켜지네

colima 끔

도커데스크탑만 켜둔 상태 (참고로 cli로 하면 desktop켜고 싶어도 colima끈상태로는 접속안됨)

❯ docker exec -it mysql-container bash
bash-4.4# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \\g.
Your MySQL connection id is 15
Server version: 8.0.33 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.

mysql>

접속 성공! 

도꺼데스크탑 껐음

데이터베이스 서버 죽어서 연결끊김

 

결론

나는 Colima를 통한 도커 환경과 

도터 데스크탑 환경 이렇게 두가지 환경을 구축해놨었다.

mysql 계정 설정했던 환경은 도커 데스크탑 환경이었어서 colima를 통해 띄운 도커 컨테이너로는 접속이 안되었던 것이다. 

 

Colima 는? 도커 컨테이너를 맥이랑 리눅스에서 실행하기 위한 오픈소스다.

https://github.com/abiosoft/colima

 

나는 왜 colima를 설치했었나..

맞다 오라클 공부해보려고 설치하는데 도커 데스크탑으로는 안된다고 해서 colima를 통해 깔았음

이후로 도커를 안써서 까먹고 있었네 딱콩!

 

나의 환경

 

 

도커 자체가 엔진이라서 다른 환경에서 쓸 수 있었다

도커 데스크탑을 통해서 도커 엔진을 윈도우, 맥환경, 리눅스에서 쓸 수 있다고 한다. 

이렇게 도커 데스크탑을 쓰다가 이슈가 터져서 사람들이 대안으로 많이쓰는게 지금

podman, colima..등등 있다고 함

https://konfuzio.com/en/docker-desktop-alternative/

 

 

 

 

읽은 글들

 

[Docker] 맥북M1에 도커 컨테이너 colima설치

 

[Docker] 맥북M1에 도커 컨테이너 colima설치

ㅁ 들어가며 Docker Desktop의 이용약관을 보면 대기업에서는 이제 무료로 사용할 수 없게 되었습니다. 그리고 Intel 칩에서만 가동되었던 오라클 컨테이너의 경우 M1에는 돌아가지 않는 문제점도 있

peterica.tistory.com

도커데스크 업데이트 내용

Docker is Updating and Extending Our Product Subscriptions | Docker

 

Docker is Updating and Extending Our Product Subscriptions | Docker

Learn from Docker experts to simplify and advance your app development and management with Docker. Stay up to date on Docker events and new version

www.docker.com

 

Difference between Docker Desktop and Docker Engine

 

Difference between Docker Desktop and Docker Engine

Technically the engine can run without the cli-client being installed. Furthermore, the cli-client can be installed on a system without the engine and use it with a remote engine. The cli-client is nothing than a user interface that interacts with the api

forums.docker.com

 

Why does Colima failed to find Docker daemon

 

Why does Colima failed to find Docker daemon

I was using Docker Desktop on my Mac for a long time. Currently Docker desktop causing a huge issues related to starting up. Everytime it needs some tweak to run or uninstall and install it again, ...

stackoverflow.com

 

What is difference between Docker and Docker Desktop?

 

What is difference between Docker and Docker Desktop?

I am using a mac and apparently installing Docker Desktop is mandatory to use Docker in macOS. Why Docker Desktop is required in Mac and Windows OS, and not in Linux? What purpose Docker Desktop se...

stackoverflow.com

 

 

homebrew에서 docker를 설치하면 기본으로 도커 데스크탑으로 설치됨 

How to use Docker without Docker Desktop on MacOS 📦

 

How to use Docker without Docker Desktop on MacOS 📦

Preamble I think a lot of large software teams have struggled with Docker's licensing...

dev.to

 

도커(Docker) 입문편: 컨테이너 기초부터 서버 배포까지

 

도커(Docker) 입문편: 컨테이너 기초부터 서버 배포까지

도커(Docker)는 2013년 등장한 컨테이너 기반 가상화 도구입니다. 도커를 사용하면 컨테이너를 쉽게 관리할 수 있으며, 이미지를 만들어 외부 서버에 배포하는 것도 가능합니다. 이 글은 도커를 시

www.44bits.io

 

 

 

728x90
728x90

프로젝트를 진행하면서 자꾸만 멈칫하게 되는 순간이 있었다. 

개선하고 싶은데 이게 맞나..? 라는 의구심이 들 때 그렇다.

 

일단 기능을 구현해놓고 객체를 분리하는 방식으로 코드를 수정했다. 

(같이 스터디하는 사람들이 DDD로 도메인과 엔티티를 따로 만드는 걸 보면서는 설계를 먼저 해놓고 코드를 짜는 것이 맞다고 느껴졌지만 나는 프로젝트가 잘 돌아갈까-라는 의심이 남아있을 때 다음 단계로 넘어가는 일이 더욱 더뎌지기 때문에..)

 

 

나는 현재 mybatis(SQLmapper:객체와 sql의 필드를 매핑하여 데이터를 객체화하는 기술)를 사용하기 때문에

sql문을 작성해 그 쿼리 수행 결과를 내가 설정한 객체에 매핑한다.

이 sql쿼리와 매핑할 객체를 Clothes 도메인 객체로 사용하려 했다.

 

그리고 서비스 단에서

Clothes 도메인 객체에 담겨있던 값들을 컨트롤러에 응답해주기 위한 응답 dto객체로 담는다.

문제는 쿼리 결과로 받은 데이터가 내가 설정한 객체에 매핑되지 않았다!

 

그 이유는 다음과 같다.

도메인 객체는

Clothes 옷 객체로

@Builder
@Getter
public class Clothes {
    private ClothesCategoryEnumType clothesCategoryEnumType;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;
    
 }

다음과 같은 필드값을 가지고 있다. 이중 옷카테고리를 갖고 있는 이넘타입이 있다.

@Getter
public enum ClothesCategoryEnumType {
    TOPS("상의"),
    PANTS("하의"),
    OUTERWEAR("겉옷"),
    ACTIVE("활동복");

    private String clothesCategoryName;

    ClothesCategoryEnumType( String clothesCategoryName) {
        this.clothesCategoryName = clothesCategoryName;
    }

}

 

하지만 실제 데이터베이스에서는

Clothes 테이블은

Clothes_category_id 열을 외래키값으로 갖고 있다.

 

Clothes_category 테이블에서 이렇게 옷카테고리를 관리하고 있다.

이 상태에서 내가 값을 가져오려고 한다면!

54번 옷장에 있는 옷을 다 불러 오려는데 등장한 에러

    @Select("SELECT * FROM Clothes  where userCloset_id = #{UserClosetId}")
    List<Clothes> selectClothes(@Param("UserClosetId") int userClosetId);
{
    "status": "BAD_REQUEST",
    "exceptions": [
        "Error attempting to get column 'id' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.favorite.project.ClothesCategory.ClothesCategoryEnumType.4"
    ],
    "message": "매핑 실패"
}

 

 

No enum constant 찾아보니 DB에서 가져온 값을 객체화하려고 보니

enum타입에 없어서 생기는 문제라고 한다

즉, enum타입에 없는 값이 DB에 있다는 것..

 

 

이넘상수객체를 필드로 참조하고 있는 환경과

외래키로 값을 참조하고 있는 데이터베이스 환경을 어떻게 맞춰줄까라는 고민에 빠졌다. 

 

 

사실상 문제의 시작은 

옷장에 옷Clothes를 넣을 때부터 시작되었다. 

 

서비스 단에서 데이터 삽입 시에

 

public int getClothesCategoryId(ClothesCategoryEnumType clothesCategoryEnumType) {
   int idByCategoryName = categoryMapper.selectCategoryIdByCategoryName(clothesCategoryEnumType.name());
    return idByCategoryName;

}

 

이렇게 clothes 테이블에 clothes_categoryid를 enum인 카테고리 이름으로 

clothes_category테이블에 있는 id값을 찾아서 값을 넣었는데 

 

이제 clothes 테이블  값을 전체 가져오려고 하니까 문제가 생긴다.

 

가져온 clothes_categoryid값은 enum 객체에는 없는 값이고 clothes_category테이블을 거쳐서 그 상수값이 들어와야하기 때문이다.

 

enum 하나 때문에 매핑에 실패해서 다른 값들을 못불러오고 있어서

문제되는 enum값 제외하고 호출해보았다.

(이래가지고 select * from말고 필요한 데이터 선언해서 불러와야 문제점을 파악하기 쉽겠다는 깨달음도 얻었다.)

 

 

 Clothes테이블에서 sql타입이 카테고리 id가 int로 선언한 dto 객체에

매핑하기 위해서 SQL문과 매핑하는 객체를 다음과 같은 형식으로 바꾸면 값이 가져와진다

@Builder
@Getter
public class ClothesPriceAndImgAndPurchasedDate {
    private int id; 
    private int clothesCategoryEnumType; //이렇게 int값으로
    private int price;
    private String img;
    private LocalDate purchasedDate;

}

 

옷장에 옷 데이터 삽입할 때는 어떻게 했을지 코드를 보자면,

나는 이러한 객체와 데이터베이스의 다른 구조를 어떻게 해결해야될지 몰라서

id를 세터로 직접 지정하는 선택을 했는데 이게.. 이게.. 맞아?

@Builder
@Getter
public class Clothes {
    private ClothesCategoryEnumType clothesCategoryEnumType;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;
    @Setter
    private int categoryId;
    //TODO: 세터로 박지 말고 다른 방법 찾기
    @Setter
    private int userClosetId;
    
 }

ClothesService 클래스

    public ClothesResponseDto addClothes(ClothesAddDto clothesAddDto) {
        Clothes clothes = clothesAddDto.toClothes(clothesAddDto);
        boolean checkValidClosetResult = checkValidCloset(clothes);

        if (checkValidClosetResult) {

            //이넘타입 이름 가져와서
            ClothesCategoryEnumType categoryEnumType = clothes.getCategoryName();
            //카테고리 테이블에서 검색해서
            int clothesCategoryId = clothesCategoryService.getClothesCategoryId(categoryEnumType);
            //그 id로 카테고리 id값을 넣고 있구나
            clothes.setCategoryId(clothesCategoryId);
            clothesMapper.insertClothes(clothes);
            return clothes.toClothesResponseDto(clothes);

        } else {
            throw new IllegalArgumentException("유효한 옷장이 아닙니다");

        }

    }

 

 

내가 원하는 것은 객체지향적인 프로그래밍이다. 

 

목표:

밖에서 clothes의 clothesCategoryId를 함부로 설정할 수 없게 객체 내부에서 알아서 처리해줬으면 좋겠다.

 

 

ClothesCategoryEnumType객체 내에서 

상수타입 → id로 변환해서 SQL문에 적용하면 어떨까 생각해봤다. 

그러면 상수타입을 Clothes_category 테이블에서 조회해서 id를 가져와 보내줘야 하지 않을까 싶어

지금처럼 카테고리 id값을 객체에 설정해주었다. 

 

id값을 직접 설정하는 것이 유연하지 않은 코드라고 느껴졌다. 그럼에도 id를 추가했던 이유는 상수타입 TOPS들고 카테고리 테이블을 다시 조회해서 id값을 들고 오기 위해  sql요청을 두 번하는 것보다는 상수객체에서 관리하는 게 낫겠다는 생각이 들어서였다.

 

이제 카테고리타입을 알면 그 카테고리id를 알 수 있게 되었다.  

public enum ClothesCategoryEnumType {
    TOPS(1, "상의"),
    PANTS(3, "하의"),
    OUTERWEAR(4, "겉옷"),
    ACTIVE(9, "활동복");

    private int clothesCategoryId;
    private String clothesCategoryName;

    ClothesCategoryEnumType(int clothesCategoryId, String clothesCategoryName) {
        this.clothesCategoryId = clothesCategoryId;
        this.clothesCategoryName = clothesCategoryName;
    }

    public static String getClothesCategoryName(int clothesCategoryId) {
        for (ClothesCategoryEnumType type : values()) {
            if (type.clothesCategoryId == clothesCategoryId) {
                return type.clothesCategoryName;
            }

        }
        return null;
    }

}

 

 

그런데 계속 막히는 지점들이 있다.

 

데이터 조회시 특정 옷장의 모든 옷들을 가져온다.

조회마다 다른 api를 작성해야하는 걸까? (결론은 yes)

Clothes 도메인으로 값을 모두 가져온다음에

사용할 데이터만 다른 dto로 담아서 컨트롤러에 응답하면 되려나? ( 결론은 쿼리매핑하는 dto객체를 별도로 만들어서 사용했다 )

 

위에도 언급했지만 지금 내 프로젝트는 JPA를 사용하고 있지 않기 때문에

데이터베이스 테이블과 매핑하는 entity개념이 빠져있다. 그래서 더 헷갈렸었다.

 

 

1. setter제거하기

 

 

setter를 제거하고 어떻게 id값을 설정할지는

Clothes 빌더로 생성할 때 설정하는 것으로 해결하였다.

 

 

 

서비스단에서 

이름을 가져와서 id를 찾았던 코드

id값을 따로 설정했던 코드를 삭제했다

 

2. SQL문과 매핑할 객체 만들기

 

데이터 조회 수정하려는데 객체와 mysql문 매핑할 때

객체 필드와 컬럼 순서가 맞아야했다!!! 사용하지 않는 필드도 들어가있으면 컬럼을 가져오지 못했다!)

{
    "status": "BAD_REQUEST",
    "exceptions": [
        "Error attempting to get column 'price' from result set.  Cause: java.lang.IllegalArgumentException: No enum constant com.favorite.project.Clothes.SeasonType.500.00"
    ],
    "message": "매핑 실패"
}

 

 

 

쿼리와 매핑하는 객체

@Builder
@Getter
public class ClothesListDto {
    private int id;
    private int userClosetId;
    private int clothesCategoryEnumTypeId;
    private int price;
    private String img;
    private LocalDate purchasedDate;
    private SeasonType season;
    
}

 

 

를 바로 컨트롤러에서 응답해주지 않고

응답객체로 다시 담아서 컨트롤러에 반환되도록 했다.

 

 

기존에 사용하고 있는 응답객체인 ClothesResponseDto에 담아볼까~~ 했는데

ClothesResponseDto의 필드 타입이 ClothesCategoryEnumType이었다.

 

int로 값을 받아왔는데..

 

삽입을 하고 결과 값을 응답할 때는

Clothes 객체 필드에서는 ClothesCategoryEnumType가 선언되어 있어서

그 필드값으로 clothesResponseDto에 이넘 객체값을 설정해줄 수 있었는데

 

조회하고 받은 내가 가진 것은 int뿐인데 이넘 객체를 어떻게 부를까…흠흠

아니면 응답객체를 새로 만들까?

 

라는 고민 속에서

 

ClothesResponseDto 대신 조회시 받는 응답객체는 따로 만들기로 했다.

요청별로 dto를 나누기 위해서 만들 생각이었기 때문에!

2-1. UserClosetResponseDTO 옷장별로 모두 응답받는 객체를 만들었다.

@Getter
@Builder
public class UserClosetResponseDTO {

//ClothesCategoryEnumType 객체의 getClothesCategoryName은 static메서드라서 선언안해도 쓸 수 있음
//    private ClothesCategoryEnumType clothesCategoryEnumType;
//ClothesListDto에서 받아와서 필드에 넣을 필요 없음
//    private int clothesCategoryTypeId;

    private String clothesCategoryEnumTypeName;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public UserClosetResponseDTO toUserClosetIdRequestDto(ClothesListDto clothesListDto) {

        return UserClosetResponseDTO.builder().clothesCategoryEnumTypeName(getCategoryEnumTypeName(clothesListDto.getClothesCategoryEnumTypeId()))
                .seasonType(clothesListDto.getSeason())
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

    private String getCategoryEnumTypeName(int clothesCategoryTypeId) {
        return ClothesCategoryEnumType.getClothesCategoryName(clothesCategoryTypeId);

    }
}

메서드도 중복되는 작업이라서 뺐다.

@Getter
@Builder
public class UserClosetResponseDTO {
    private String clothesCategoryEnumTypeName;
    private SeasonType seasonType;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public UserClosetResponseDTO toUserClosetIdRequestDto(ClothesListDto clothesListDto) {

        return UserClosetResponseDTO.builder()
                .clothesCategoryEnumTypeName(ClothesCategoryEnumType.getClothesCategoryName(clothesListDto.getClothesCategoryEnumTypeId()))
                .seasonType(clothesListDto.getSeason())
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

}

 

이제 ClothesService내에서 변환로직을 사용해보자~~

리스트를 만들어서 담았다

    public List<UserClosetResponseDTO> getAllClothes(int userClosetId) {
        List<ClothesListDto> clothesListDtos = clothesMapper.selectClothes(userClosetId);
        List<UserClosetResponseDTO> userClosetResponseDTOList = new ArrayList<>();
        for (ClothesListDto clothesListDto : clothesListDtos) {
            UserClosetResponseDTO userClosetResponseDTO =
                    UserClosetResponseDTO
                            .builder()
                            .build()
                            .toUserClosetIdRequestDto(clothesListDto);
            userClosetResponseDTOList.add(userClosetResponseDTO);
        }

        return userClosetResponseDTOList;

    }

 

2-2. 마찬가지로 계절별로 응답 객체를 만들고 계절별로 조회 로직을 수정했다.

응답 객체에서 확실히 다른 점은

계절별로 조회하기 때문에 userClosetId 값도 담을 것이다

sql쿼리에 userClosetId도 가져올 수 있도록 수정했다

 

계절별로 응답 객체

@Getter
@Builder
public class SeansonResponseDto {
    private int userClosetId;
    private int clothesId;
    private String clothesCategoryEnumTypeName;
    private int price;
    private String imgUrl;
    private LocalDate purchasedDate;

    public SeansonResponseDto toSeansonResponseDto(ClothesListDto clothesListDto) {

        return SeansonResponseDto.builder()
                .userClosetId(clothesListDto.getUserClosetId())
                .clothesId(clothesListDto.getId())
                .clothesCategoryEnumTypeName(ClothesCategoryEnumType.getClothesCategoryName(clothesListDto.getClothesCategoryEnumTypeId()))
                .price(clothesListDto.getPrice())
                .imgUrl(clothesListDto.getImg())
                .purchasedDate(clothesListDto.getPurchasedDate())
                .build();
    }

}

 

마무리

데이터 삽입, 조회 시에

  1. 데이터베이스 구조와 다른 객체 구조를 어떻게 다룰지
  2. 데이터전송객체 dto를 어떻게 분리하고 사용할지에 대해 고민하고 수정해보았다.
728x90
728x90

<단위 테스트 - 생산성과 품질을 위한 단위 테스트 원칙과 패턴>를 읽고 4장을 정리한 내용이다. 

 

가치 있는 테스트를 식별하는 것과

가치있는 테스트를 작성하는 것을 별개의 기술이다.

가치있는 테스트를 식별하는 방법을 이 장에서 알아볼 것이다.

4.1. 좋은 단위 테스트의 4대 요소

  • 회귀 방지
    • 테스트가 얼마나 버그를 잘 찾아 내는지
  • 리팩터링 내성
  • 빠른 피드백
    • 얼마나 빠른지
  • 유지 보수성

4.1.1 회귀 방지

코드는 자산이 아니라 책임이다.

코드베이스가 커질 수록 잠재적인 버그에 더 많이 노출된다.

회귀에 대해 효과적인 보호를 개발하는 것이 중요하다

회귀방지 지표에 대한 테스트 점수가 잘 나오는지 평가하기 위해 고려해야할 것

  • 테스트 중에 실행되는 코드 양
  • 코드 복잡도
  • 코드의 도메인 유의성

단순한 코드를 테스트하는 것은 가치가 없다. 버그를 잘 찾을 수 있는 것이 중요. 회귀 오류를 잘 찾는 것이 중요

회귀 방지 지표를 극대화 하려면 테스트가 가능한 많은 코드를 실행하는 것을 목표로 해야한다.

외부시스템에 의존하는 코드 즉 의존성에 대해 검증하는 코드도 중요하다

4.1.2 리팩터링 내성

리팩터링은 식별할 수 있는 동작을 수정하지 않고 기존 코드를 변경하는 것을 의미한다.

기존 코드를 변경하더라도 문제없이 코드가 동작해야한다.

기능은 문제가 없는데 기반 코드를 수정했을 때 테스트가 실패되었을 때의 결과를

거짓 양성이라 한다.

거짓양성은 허위정보이다.

리팩터링 내성 지표에서 테스트 점수가 잘 나오는지 평가하려면

거짓 양성 발생이 얼마나 적은지 보면 된다.

거짓 양성은

  • 타당한 실패도 무시하게 된다.
  • 테스트에 대한 신뢰도를 낮춘다.

4.1.3 거짓 양성의 원인이 무엇인가?

거짓 양성 수는 테스트 구성방식과 직접 관련이 있다.

테스트와 테스트 대상 시스템 sut의 구현 세부사항이 많이 결합할수록 허위 경보가 많이 생긴다.

구현 세부사항에서 테스트를 분리해야 거짓 양성을 줄일 수 있다.

테스트는 최종 사용자의 관점에서 sut를 검증해야하고

최종 사용자에게 의미있는 결과만 확인해야한다.

리팩터링 과정은 애플리케이션의 식별할 수 있는 동작에 영향을 주지 않으면서 구현을 변경하는 것인데

테스트를 구현에 결합하게 되면 구현을 리팩터링하면 모두 테스트 실패로 이어진다.

4.1.4 구현 세부사항 대신 최종결과를 목표로 하기

sut의 구현 세부사항과 테스트간의 결합도를 낮추는 것이 리팩토링 내성을 높이는 방법이다.

4.2 회귀방지와 리팩터링 내성 사이의 관계

둘다 테스트 스위트의 정확도에 기여한다.

프로젝트가 시작된 직후에는 회귀 방지를 잘 갖추는 것이 중요하지만

리팩터링 내성은 바로 필요하지 않다.

  • 테스트 정확도 극대화
  • 거짓양성과 거짓 음성의 중요성

4.2.1 테스트 정확도 극대화

테스트 통과, 기능 작동 : 올바른 추론 (참 음성)

테스트 통과 , 기능 고장 : 2종 오류(거짓음성)

테스트 실패, 기능 작동 : 1종 오류 (거짓 양성)

테스트 실패, 기능 고장 : 올바른 추론 (참 양성)

 

거짓 음성을 피하는데 좋은 테스트의 첫번째 특성인 회귀방지가 도움된다.

거짓 양성을 피하는데는 리팩터링 내성이 도움된다,

거짓양성과 거짓음성의 확률로 테스트 수준을 나타낼 수 있다. 즉 확률이 낮을 수록 테스트가 정확한다.

(아래 설명으로 이해함)

회귀 방지와 리팩터링 내성은 테스트 정확도에 기여한다.

테스트는 가능한 한 (리팩토링 내성 영역) 적은 소음(거짓양성) 으로 강한 신호(버그를 찾을 수 있음, 회귀 방지영역)을 발생시키기 때문에 정확하다.

정확한 지표는 다음 두가지 요소로 구성된다.

  • 테스트가 버그있음을 얼마나 잘 나타내는가 (거짓음성 제외 → 거짓음성이 적다는거겠지? )
  • 테스트가 버그 없음을 얼마나 잘 나타내는가. (거짓 양성 제외 → 적다는 거겠지? )

테스트 정확도 = 신호 (발견된 버그수) / 소음(허위 경보 발생 수)

4.2.2 거짓 양성과 거짓 음성 중요성: 역학 관계

둘다 나쁘지만

거짓 양성은 프로젝트가 커지면서 점점 더 큰 영향을 미치기 시작한다.

대부분 당장의 버그를 잘 잡는 회귀방지에만 중점을 두지만 프로젝트가 커지면서 거짓 양성에도 주의를 기울여야 한다.

4.3 세번째 요소와 네번째 요소 빠른 피드백과 유지 보수성

  • 빠른 피드백
    • 단위 테스트의 필수 속성
  • 유지 보수성

유지보수성 지표는 유지비를 평가한다.

  • 테스트가 얼마나 이해하기 어려운가. 테스트코드를 일급시민으로 취급하라
  • 테스트가 얼마나 실행하기 어려운가. 외부종속성으로 작동하면 그만큼 운영비가 든다.

4.4 이상적인 테스트를 찾아서

네 가지 특성을 모두 곱하면 테스트 가치가 결정된다.

즉 하나의 특성이 0이면 모두 0이다. 네 가지 범주에서 모두 점수를 내야한다.

테스트 코드를 포함한 모든 코드는 책임이다. 최소 필수값에 대해 상당히 높은 임계치를 설정하고 임계치를 충족한느 테스트만 테스트 스위트에 남겨라. 소수의 가치있는 테스트가 더 효과적이다.

4.4.1 이상적인 테스트를 만들 수 있나?

회귀방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이다.

셋 중 하나를 희생해야 나머지 둘을 최대로 할 수 있다.

엔드 투 엔드 경우 최종 사용자의 관점에서 시스템을 살펴본다.

많은 코드를 테스트하여 회귀방지를 잘해낸다.

하지만 그만큼 속도가 느려 피드백을 빨리 받을 수 없다.

너무 간단한 테스트는 다양한 케이스를 검증할 수 없어 버그를 찾울 수 없기에 회귀를 나타내지 않는다.

깨지기 쉬운 테스트는 실행이 빠르고 회귀를 잡을 가능성이 높지만 거짓양성이 많은 테스트이다.

구현에 결합되어 있어 리팩터링 내성이 거의 없다

4.4.5 이상적인 테스트를 찾아서 결론

회귀방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이다.

한가지특성을 희생해야한다.

네 번째 특성 유지보수성은 엔드투엔드 테스트를 제외하고 세가지 특성과 상관관계가 없다. (엔드투엔드는 모든 의존성을 설정하고 운영해야하므로 유지보수 비용이 많이 든다. )

그렇다면 어떤 특성을 희생해야하는가

리팩터링 내성은 포기할 수 없다. 최대한 많이 갖는 것을 목표로 해야한다.

결국 회귀방지와 빠른 피드백 사이에서 절충해야한다.

리팩터링 내성 포기할 수 없는 이유는 이진선택이기 때문에 있거나 없거나 둘 중 하나이다.

즉, 테스트 스위트를 탄탄하게 만들기 위해서는 테스트의 불안전성(거짓 양성)을 제거하는 것이 최우선 과제이다.

4.5. 대중적인 테스트 자동화 개념

테스트 피라미드는 테스트 스위트에서 테스트 유형간의 일정한 비율을 일컫는 개념이다

  • 엔드투 엔드 테스트
  • 통합테스트
  • 단위테스트

위에 있는 앤드투 앤드 테스트가 가장 사용자의 동작과 유사하게 흉내내고

아래에 있는 단위 테스트가 테스트 수가 가장 많다.

피라미드 내 테스트 유형에 따라 빠른 피드백과 회귀방지 사이에서 선택한다.

피라미드 상단의 테스트는 회귀방지에 유리 (여러 케이스로 버그 확인)

하단은 실행속도를 강조한다 (단위테스트는 빠른 피드백 강조)

통합테스트는 그 중간에 있다.

하지만 어느 계층도 리팩터링 내성을 포기하지 않는다!

테스트 유형간의 정확한 비율은 팀과 프로젝트마다 다르지만 일반적으로 피라미드 형태를 유지해야한다.

4.5.2 블랙박스 테스트vs 화이트박스 테스트간의 선택.

블랙박스테스트는 시스템 내부구조 몰라도 시스템의 기능을 검사할 수 있는 테스트 방법이다.

어떻게가 아닌 무엇을 해야하는지가 중심

화이트박스 테스트는 내부를 검증하는 테스트 방식이다

소스코드에서 파생

결론적으로 모든 테스트가 시스템을 블랙박스로 보게 만들고 문제영역에 의미있는 동작을 확인해야한다.

테스트를 분석할 때는 화이트 박스, (코드 커버리지 도구를 사용해서 어떤 코드분기를 실행하지 않았는지 확인)하고

코드 내부 구조에 대해 전혀 모르는 것처럼(블랙박스)로 테스트하라

이런 조합이 가장 효과적이다

728x90
728x90

데이터베이스 스터디로 러닝MySQL을 읽고 있다. 

공부한 내용을 정리해보겠다!

 

2.3 관계형 엔티티 모델

 

데이터베이스는

객체인 엔티티

엔티티간의 연결정보인 관계를 저장한다.

 

예) 엔티티는 제품과 고객, 둘의 관계가 판매

 

관계형 엔티티 entity relationship ER모델은

개념적 설계의 일반적인 접근 방식으로 요구사항을 디비의 관계와 엔티티의 형식적인 설명으로 변환할 때 도움이 된다.

 

 

2.3.1 엔티티 표현 방식

설계를 시각화하기 위해 ER모델링에는 ER다이어그램을 그리는 과정이 포함된다.

일반적으로 엔티티의 특징이나 속성을 저장하기 위헤 데이터베이스를 사용한다.

 

엔티티에 속한 속성은 해당 엔티티를 설명한다.

속성을 더 세분화해서 구성할 수 있다.

속성이 더 작은 단위로 구성된 경우 : 복합 속성

그렇지 않은 경우 : 단순 속성으로 분류

 

 

속성은 값이 동일한 엔티티를 구별하는데 도움이 된다. 고객을 구별하기 위해 각자 고유한 특성이 필요한데

이런 고유한 키 값을 기본 키라고 부른다.

기본 키 설정에는 신중해야한다.

예) 이메일을 기본키로 하면 , 이메일이 여러 개인 고객은? 이메일이 없는 고객은?

 

대체키로 사용할 수 있는 또 다른 속성을 보자면, 두 명의 고개개이 전화번호가 동일할 수 있으나 (전화번호는 기본키로 사용할 수 없음) 번호가 같아도 고객 이름이 같지 않기 때문에 전화번호와 이름을 복합 키로 사용할 수 있다.

엔티티로 식별할 때 사용할 수 있는 키가 여러 개일 수 있다.

대안 중 하나를 선택하거나 후보키 를 기본 키로 선택한다.

출처: https://jerryjerryjerry.tistory.com/49
키: 무언가를 식별하는 고유한 식별자 키의 종류로 기본키 , 슈퍼키, 대체키, 외래키 등이 있다.

슈퍼키 super key: 각 행을 유일하게 식별할 수 있는 하나 또는 그 이상의 속성들의 집합이다. 즉 유일성(고유한 데이터 속성)만 만족하면 슈퍼키가 될 수 있다. ex. 주민번호, 이름+나이, 학번

후보키 candidate key: 각 행을 유일하게 식별할 수 있는 최소한의 속성들의 집합 후보키는 기본키가 될 수 있는 후보들이며 유일성과 최소성을 동시에 만족해야한다. ex. 주민전호, 학번, (이름+ 나이 는 2개의 속성으로 되어있어서 후보키가 될 수 없다. )

기본키(primary key: 후보키들 중에서 하나를 선택한 키로 최소성과 유일성을 만족하는 속성이다. 테이블에서 기본키는 오직 1개만 지정할 수 있다. null값을 절대 가질 수 없고, 중복된 값을 가질 수 없다.

대체키 alternate key:후보키가 두개 이상일 경우 그중에서 어느 하나를 기본키로 지정하고 남은 후보키들을 대체키라고 한다. 기본키로 선정되지 않은 후보키이다.

외래키 Foreign key: 테이블이 다른 테이블의 데이터를 참조하여 테이블 간의 관계를 연결하는 것. 다른 테이블의 데이터를 참조할 때 없는 값을 참조할 수 없도록 제약을 준다.

부모 테이블 먼저 삭제될 수 없다. 왜냐하면 부모테이블을 참조하는데 부모테이블이 삭제되면 자식테이블은 참조하는 것이 없어지기 때문에 외래키 오류가 생긴다. 외래키 관계에서 부모테이블을 삭제하려면 자식테이블 먼저 삭제한 후 부모테이블을 삭제해야한다. 

 

2.3.2 관계 표현

엔티티는 다른 엔티티와 관계를 가질 수 있다.

에) 고객은 제품을 살 수 있고, 학생은 수업을 수강할 수 있고, 직원은 주소를 가질 수 있다.

엔티티와 마찬가지로 관계 또한 속성을 가질 수 있다.

예) 판매를 고객 엔티티(고유한 이메일 주소로 식별됨) 와 특정 날짜 및 시간(타임스탬프)에 존재하는 몇 가지 제품 엔티티(고유한 제품ID값으로 식별됨) 사이의 관계로 정의할 수 있다. 관계의 양족에서 엔티티 수가 다를 수도 있다.

  • 다대다 
    • 고객이 원하는 수의 제품을 구입할 수 있고, 제품이 원하는 수의 고객에게 구입될 수 있다.
    • M:N
  • 일대다
    • 한 사람이 신용카드 여러개는 되지만 각 신용카드는 한 사람에게만 속함
    • 다대일 N:1
    • 여러개의 신용카드가 한사람에게 속해짐
  • 일대일 1:1
    • 자동차 엔진의 일련번호, 엔진에는 단 하나의 일련번호만 부여된다.
    • 일련번호는 하나의 자동차 엔진에만 속한다.

관계의 양쪽에 있는 엔티티의 수 (관계의 카디널리티cardinality)는

관계의 주요 제약 조건constraints을 정의한다.

관계의 카디널리티에 대해 신중히 생각할 것.

처음에는 일대일처럼 보이지만 나중에 복잡해즌 관계들이 많다.

예) 사람들이 때때로 이름을 바꿈, 경찰 디비에서는 더욱 그런 데이터가 있을 수 있다. 사람엔티티와 이름 엔티티를 다대다 관계로 모델링

 

중복된 값이 적을수록 좋다

주민번호를 조회하는 것처럼 중복 수치가 낮으면 빨리 데이터를 찾을 수 있기 때문

중복도가 낮을수록 카디널리티가 높다

중복도가 높으면 카디널리티가 낮다

 

카디널리티란?
테이블의 총 행 수에 대한 관계형 테이블 열의 고유 값 수

 

2.3.3 부분 참여와 전체 참여

엔티티 관계는 선택사항이거나 필수 사항이다.

선택사항 - 고객 엔티티는 구매 관계에 완전히 참여함

모든 고객이 제품을 구매했고 제품을 구매하지 않은 고객은 있을 수 없다.

부분적으로 참여 -고객이 제품을 구입할 수 있다.

 

즉 

부분참여가 비식별관계

전체참여가 식별관계

와도 같다고 한다. 

 

즉 부분적으로만 참여를 하여 값이 있어도 되고 없어도 되지만

ex. 회원가입시 유저의 주소 적어도 되고 안적어도 된다. 

전체참여는 식별을 할 수 있도록 필수적으로 있어야한다. 

ex. 회원가입시 유저의 주소가 필수적이다. 

 

 

요즘 추세는 식별/비식별관계를 잘 쓰지 않는다고 한다. 

외래키로 다른 테이블과 식별/비식별관계(Null일 수 있음)인 경우 

상위객체(소유엔티티)가 존재할 때 하위객체가 존재해야하기 때문에 

외래키 데이터를 삭제할 때 "참조 무결성 제약 조건 위반"이  발생할 수 있다. 

 

그래서 식별/비식별 대신

관계를 맺지 않고 키값을 갖는다고 한다. 그리고 인덱스를 걸어서 처리.

(인덱스를 건다는 것은 데이터베이스에서 특정 값을 빠르게 찾을 수 있게 기존 데이터를 정렬해놓아 검색할 수 있도록 하는 것)

 

 

 

2.3.4 엔티티 또는 속성

항목이 그 자체로 속성인지 아니면 엔티티인지 구분하기 어려운 경우가 있다

예) 속성이라 생각했던 이메일 주소는 그 자체로 엔티티가 될 수 있다.

이럴 경우 규칙을 사용해보기

  • 데이터베이스와 직접적인 관련이 있는가?
    • 직접적인 관계의 객체는 엔티티
    • 엔티티를 설명하는 정보는 속성에 저장되어야한다.
    • 예) 재고 및 판매 데이터베이스에서 이메일 주소 보다 고객이 중요하므로 이메일 주소는 엔티티보다는 고객의 속성이 되는 것이 좋다.
  • 항목 자체를 구성하는 요소가 있는가?
    • 만약 그렇다면 이런 구성요소를 나타낼 방법을 찾아야 한다.
    • 별도의 엔티티를 만드는 게 좋은 해결책이 될 수 있다.
    • 예) 학생성적 디비에서 학생 엔티티로 학생이 수강하는 수업, 연도 학기 저장, 수업 엔티티 별도로 생성.
  • 객체가 여러 개의 데이터를 가질 수 있는가?
    • 만약 그렇다면 여러 개의 데이터를 저장할 방법을 찾아야한다.
    • 가장 확실한 방법은 객체를 별도의 엔티티로 나타내는 것이다.
    • 예) 고객에게 이메일 주소가 하나이상 있다면 이메일 주소를 별도의 엔티티로 모델링한다.
  • 객체가 존재하지 않거나 알 수 없는 경우가 많은가?
    • 만약 그렇다면 이 객체는 사실상 일부 엔티티의 속성일 뿐이므로 별도의 엔티티로 만드는 편이 더 좋다.
    • 예) 학생 수업 성적을 저장하기 위해 모든 수업에 대한 성적 속성을 학생 엔티티가 취할 수 있다. 그러나 대부분 학생들이 일부 수업에 대해서만 성적을 받으므로 성적을 속성값으로 관리하지 않고 성적 엔티티로 별도 엔티티 집합으로 나타내는 것이 좋다.

2.3.5 엔티티 또는 관계

객체를 엔티티로 할지 관계로 할지 결정하는 쉬운 방법은

요구사항 명사를 엔티티

동사를 관계로 바꾸는 것!

예) 학위 프로그램은 하나 이상의 과정으로 구성된다 - 문장에

프로그램, 과정 - 엔티티 | 구성됨 - 관계

예) 학생이 프로그램에 등록합니다. 라는 문장에서는

학생, 프로그램 - 엔티티 | 등록 - 관계

설계를 단순하게 유지하고 가능하면 소소한 엔티티를 도입하지 말라

 

2.3.6 중간엔티티

 

다대다 관계를 새로운 중간 엔티티(연관엔티티)로 대체하고

원래 엔티티를 일대다 관계와 다대일 관계로 연결해

다대다 관계를 개념적으로 단순화할 수 있다.

 

예) 승객이 항공편을 예약합니다

승객, 항공편 - 엔티티 → 다대다 관계

 

한 승객이 예약 여러 번 할 수 있음

하지만 자리는 오직 한 승객에게 주어짐

이 관계(예약)의 카디널리티(관계의 양쪽에 있는 엔티티의 수)는 1:N

 

이와 마찬가지로

하나의 항공편에 많이 예약할 수 있지만

각 자리는 오직 한 항공편에서만 가능하므로

이 관계(배치)의 카디널리티 1:N

 

자리가 특정 승객과 특정 항공편과 연결되어야 하므로

자리 엔티티는 모든 엔티티와의 관계에 완전하게 참여한다. (자리가 필수로 있어야함!)

 

2.3.7 약한 엔티티와 강한 엔티티

약한 엔티티는 다른 엔티티에 종속되어 독립적으로 존재할 수 없으며,

식별 관계에 완전하게 참여한다. 반면 강한 엔티티는 자체적으로 고유하게 식별될 수 있는 엔티티이다.

출처: https://studyandwrite.tistory.com/427
강한 엔티티 타입은 자신의 키 속성만 이용해서 고유하게 엔티티들을 식별할 수 있는 타입
약한 엔티티 타입은 자신의 키 속성만으로 엔티티를 고유하게 식별할 수 없는 타입!

 

 

식별 관계에서 약한 엔티티는 부모 엔티티의 일부분이 되어 해당 엔티티를 식별한다. 그리고 약한 엔티티의 전체 키는 자신의 부분 키와 소유한 엔티티의 키를 조합한 값이 된다.

 

맥락을 바탕으로 적은 양의 정보로도 작업이 가능하다.

디비 설계에서는 다른 엔티티에 종속된 엔티티에 대한 주요 정보를 몇 개 생략할 수 있다.

예) 고객의 자녀 이름 저장하는 경우, 자녀 엔티티 만들고 부모를 잘 구분해주는 주요 정보만 저장할 수 있다.

이 경우 자녀 엔티티가 약한 엔티티이며

자녀엔티티와 고객 엔티티의 관계를 식별관계 라고 한다.

약한 엔티티는 데이터베이스에서 소유 엔티티와 독립적으로 존재할 수 없기 때문에 식별 관계에 완전하게 참여한다.

 

약한 엔티티는

해당 엔티티를 소유한 즉 강한 엔티티의 맥락 안에서

고유하게 구분되므로

약한 엔티티의 전체 키는

자신의 키(부분)와 소유한 엔티티의 키를 조합한 값이다

 

 

2.4 데이터베이스 정규화

데이터베이스 정규화는 관계형 디비의 구조를 설계할 때 중요한 개념이다

정규형의 목적 : 데이터의 중복을 줄이고 무결성을 향상하는 것

또한 정규화는 데이터베이스 구조를 재설계하거나 확장하는 프로세스를 효율화한다.

 

세 가지 정규형의 목표를 살펴본다

제 1정규형 1NF : 정규형(Normalization Form)

  • 개별 테이블에서 반복되는 그룹을 제거한다
  • 관련 데이터 집합에 대해 별도의 테이블을 만든다
  • 기본 키로 각 관련 데이터 집합을 식별한다.

만약 관계에 복합 또는 다중 속성이 포함된다면 제 1정규형을 위반한 것이다.

반대로 복합 또는 다중 속성이 포함되지 않는다면 제1정규형을 만족한다.

따라서 해당 관계의 모든 속성값이 단일 타입인 경우 이를 제 1정규형이라고 한다.

제 2정규형 2NF

  • 여러 행에 적용되는 값들의 집합은 별도의 테이블로 만든다
  • 이런 테이블들은 외래키 foreign key로 연결된다.

해당 레코드는 테이블의 기본키 (필요한 경우 복합키)가 아닌 다른 것에 종속해서는 안된다

제 3정규형 3NF

  • 키에 의존하지 않는 필드를 제거한다

해당 레코드 키의 일부가 아닌 값이 테이블에 없어야한다. 일반적으로 필드 그룹의 내용이 단일 레코드 이상에 적용될 수 있는 경우 해당 필드를 별도의 테이블에 배치하는 방법을 고려해야한다.

비정규형 UNF unnormalized form :

데이터 베이스 정규형 조건을 충족하지 않는 데이터베이스 모델을 말한다.

 

 

728x90

'데이터베이스' 카테고리의 다른 글

GraphQL란?  (0) 2023.02.13
몽고디비 공부중..  (0) 2021.11.10
728x90

아래 글은 김영한님의 강의 (스프링DB-1편)으로 공부하고 트랜잭션을 사용해보면서 정리한 내용이다.

트랜잭션 격리수준에 대해서는 ([MySQL] 트랜잭션의 격리 수준(Isolation Level)에 대해 쉽고 완벽하게 이해하기 )블로그를 보았다.

 

 

Transaction 어노테이션이 없어도 동작되는 이유가 뭐지?

이 어노테이션은 언제 사용해야하는가?

 

이런 질문에 대한 답을 구하기 전에

먼저 트랜잭션 개념에 대한 정리가 필요했다. 

 

트랜잭션

  1. 트랜잭션은 무엇인가?
    1. 트랜잭션 : 거래 
    2. 트랜잭션은 이런 데이터 상의 거래를 안전하게 처리하고 보장해준다.
  2. 트랜잭션 ACID:
      1. 원자성 (Atomicity):
        • 예시: 은행 계좌 이체
        • 고객이 A 계좌에서 B 계좌로 일정 금액을 이체하는 트랜잭션을 고려하면, 이체 작업은 A 계좌에서의 출금과 B 계좌로의 입금 두 단계로 이루어진다. 이 두 단계는 원자성을 가져야 하며, 하나의 단계가 실패하면 다른 단계도 롤백되어야한다.
      2. 일관성 (Consistency):
        • 예시: 주문 및 재고 관리
        • 상품 주문 트랜잭션에서는 주문이 들어올 때마다 재고가 감소해야 한다. 이때 주문 트랜잭션이 성공하면 재고가 감소하고, 실패하면 재고는 변하지 않아야 한다. 이를 통해 데이터베이스는 일관된 상태를 유지한다.
      3. 격리성(고립성) (Isolation):
        • 예시: 동시 계좌 이체
        • 여러 사용자가 동시에 계좌 간 이체를 시도하는 상황에서, 각각의 트랜잭션이 다른 트랜잭션에 영향을 주지 않아야 한다. A 계좌에서의 이체 작업이 완료되기 전까지는 B 계좌에서의 이체 작업을 다른 트랜잭션이 간섭하지 않도록 보장되어야 한다.
        • 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
      4. 지속성 (Durability):
        • 예시: 주문 정보의 저장
        • 고객이 주문을 완료하면 해당 주문 정보는 데이터베이스에 영구적으로 저장되어야 한다. 시스템 장애 또는 다른 문제가 발생하더라도 주문 정보는 지속되어야 하며, 고객이 주문을 확인할 수 있어야 한다. 데이터베이스 트랜잭션의 특성을 나타내는 약어로 Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(지속성)을 나타낸다.

 

격리성을 완벽하게 보장하기 위해서는

트랜잭션을 순서대로 실행할 수 있지만 이렇게 동작할 경우 동시처리 성능이 나빠진다.

트랜잭션은 기본적으로 동시에 여러 개가 실행될 수 있다.

격리 수준은 이러한 동시성에서 발생할 수 있는 문제들을 어떻게 제어할 것인지를 정의한다.

즉,트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때, 특정 트랜잭션이 다른 트랜잭션에서 데이터를 변경하거나 조회할지 등 간섭할지 여부를 제어하는데 사용된다. 이처럼 격리 수준은 트랜잭션 간의 데이터 일관성을 유지하기 위해 중요하다.

 

<트랜잭션 격리 수준 Isolation level>

  1. READ UNCOMMITTED (커밋되지 않은 읽기):
    1. 다른 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있다. 이로 인해 Dirty Read(더티 리드), Non-Repeatable Read(반복 불가능한 읽기) 등의 문제가 발생할 수 있다.
      1. 활용 예시: 실시간 보고서 생성 시에 최신 데이터를 빠르게 반영하고자 할 때 사용될 수 있다.
      2. Dirty Read(더티 리드)는 다른 트랜잭션이 아직 커밋되지 않은 데이터를 읽는 현상을 말한다. 트랜잭션이 데이터를 변경하고 커밋하기 전에 다른 트랜잭션이 해당 데이터를 읽으면, 그 읽힌 데이터는 커밋되지 않아 롤백될 수 있으므로 정확하지 않거나 무의미한 데이터가 될 수 있다.
      3. 트랜잭션 격리 수준이 READ COMMITTED 이상으로 설정되면 Dirty Read 문제를 방지할 수 있다. READ COMMITTED 이상의 격리 수준에서는 다른 트랜잭션이 아직 커밋되지 않은 데이터를 읽지 못하도록 보장되기 때문이다.
  2. READ COMMITTED (커밋된 읽기):
    1. 커밋된 데이터만 읽을 수 있다. 다른 트랜잭션이 작업을 커밋하기 전까지 데이터를 읽을 수 없어 Dirty Read는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있다.
      1. 활용 예시: 데이터베이스의 무결성을 중시하는 경우에 사용될 수 있습니다.
    2. Non-Repeatable Read : 트랜잭션이 동일한 쿼리를 수행했을 때, 두 번째 읽기에서 다른 값을 반환하는 문제를 가리킨다. 이는 동일한 트랜잭션 안에서 동일한 데이터를 읽을 때 일관성이 유지되지 않는 상황을 말한다. 
      1. 트랜잭션 A:
        • 시작 시점에 데이터 X를 읽습니다. (값: 100)
      2. 트랜잭션 B:
        • 트랜잭션 A가 진행 중일 때, 데이터 X를 수정하고 커밋합니다. (값: 200)
      3. 트랜잭션 A:
        • 이어서 동일한 데이터 X를 다시 읽으려고 합니다.
        • Non-Repeatable Read 문제: 이전에 읽은 값과 다르게 새로운 값 (200)를 읽게 됩니다.
    3. 이렇게 트랜잭션이 같은 데이터를 두 번 이상 읽을 때, 중간에 다른 트랜잭션이 해당 데이터를 수정하고 커밋하면서 값이 변경되어 버리는 현상이 발생한다.이러한 상황에서는 트랜잭션 내에서 일관성이 깨지게 되어 Non-Repeatable Read 문제가 발생합니다.
    4. 트랜잭션 격리 수준이 REPEATABLE READ 이상으로 설정되면 Non-Repeatable Read 문제는 방지된다. REPEATABLE READ 이상의 격리 수준에서는 동일한 트랜잭션 안에서 같은 데이터를 여러 번 읽어도 그 값이 변경되지 않음이 보장된다.
  3. REPEATABLE READ (반복 가능한 읽기):
    1. 동일한 쿼리를 여러 번 실행해도 항상 동일한 결과를 보장한다. 다른 트랜잭션이 해당 범위의 데이터를 변경할 수 없도록 하지만 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다. 
    2. 활용 예시: 일정 시간 동안 동일한 데이터를 여러 번 읽어야 하는 작업에서 사용될 수 있습니다.
    3. REPEATABLE READ는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고해서 데이터를 조회한다.
  4. SERIALIZABLE (직렬화 가능):
    1. 트랜잭션 간에 완전한 격리를 보장한다. 다른 트랜잭션이 해당 범위의 데이터에 접근하지 못하도록 막아주어 가장 엄격한 격리 수준이지만, 접근하지 못해 트랜잭션이 순차적으로 실행되어야하기 때문에 동시성이 낮아질 수 있다. 가장 안전하고 가장 성능이 떨어진다.
    2. 활용 예시: 데이터 정합성이 최우선이 되는 금융 거래와 같이 매우 중요한 트랜잭션에서 사용될 수 있다.

 

 

트랜잭션이 동작하기 위해 필요한 것

0. 자동커밋이 아닌 수동 커밋이어야 한다. 

  • 자동커밋은 set autocommit true; 각각의 쿼리 실행 직후에 자동으로 커밋호출된다. 바로 데이터베이스에 반영되어 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없다.
  • 수동 커밋 은 set autocommit false; 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다.

1. DB 서버에 연결하기 위한 커넥션이 필요하다. 

커넥션을 생성해 맺고

2. DB서버는 내부 세션을 만든다. 

3. 커넥션을 통한 모든 요청이 이 세션을 통해 실행하게 된다. 

4. 세션은 트랜잭션을 시작하고 , 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 

5. 사용자가 커넥션을 닫거나 DBA(데이터 베이스 관리자)가 세션을 강제로 종료하면 세션은 종료된다. 

 

커넥션 풀이 10개의 커넥션을 생성하면 세션도 10개 만들어진다. 

커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이므로

해당 트랜잭션을 시작한 세션에서만 변경 데이터가 보이고 다른 세션에서는 변경데이터가 보이지 않는다.

커밋하기 전에 롤백을 하면 직전 상태로 복구된다.

 

<락 사용>

 

세션 1이 트랜잭션 시작하고 데이터를 수정하는 동안 아직 커밋하지 않았는데

세션2에서 동시에 같은 데이터를 수정하게 되면 문제가 발생한다.

이를 방지하기 위해 커밋이나 롤백전까지 다른 세션에서 해당 데이터를 수정할 수 없게 락 개념이 사용된다.

락을 획득해서 커밋후에 락을 반납하기까지 다른 세션에서는 락을 획득하기 위해서 대기하는 방식이다. 설정된 락 타임아웃 시간보다 더 오래 대기하면 락 타임아웃 오류가 발생한다.

 

조회시에 보통 락 획득하지 않고 바로 데이터를 조회할 수 있지만 조회시에도 락을 획득하고 싶다면 select for update구문을 사용하면 된다. 이렇게 되면 특정 세션이 조회할 때 락을 가져가 버리기 때문에 해당 데이터를 다른 세션에서 변경할 수 없다.

  • 조회시점에 락 필요한 경우
    • 예) 금액 조회하고 그 금액 정보로 다른 계산을 수행해야할 때

 

<트랜잭션을 어디에서 시작하고 어디에서 커밋해야하는가?>

트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야한다. 로직이 잘못되었을 시 롤백하기 위해서이다. 하지만 트랜잭션을 시작하기 위해서는 커넥션이 필요하다. 커넥션만들고 트랜잭션 커밋, 커넥션 종료까지 해야한다.

또한 같은 세션에서 작업하기 위해 같은 케넥션을 유지해아한다.

 

스프링 트랜잭션이 가져다주는 편리함은 무엇일까? 

스프링이 제공하는 트랜잭션 매니저는 

1. 추상화 

package org.springframework.transaction;
 public interface PlatformTransactionManager extends TransactionManager {
     TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
             throws TransactionException;
     void commit(TransactionStatus status) throws TransactionException;
     void rollback(TransactionStatus status) throws TransactionException;
}

 

추상화된 트랜잭션 매니저 인터페이스를 통해 

코드를 변경하지 않고 

다양한 데이터 접근 기술을 사용할 수 있다. 

 

2. 리소스 동기화

  1. 트랜잭션을 유지하기 위해서는 같은 데이터베이스 커넥션을 유지해야한다.
  2. 파라미터로 커넥션을 전달하는 것 대신 트랜잭션 동기화 매니저를 사용한다!

PlatformTransactionManager과 같은 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관한다.

리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.

트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션도 닫는다.

 

트랜잭션 동기화 매니저를 보면

쓰레드 로컬을 사용하는 것을 확인할 수 있다.

쓰레드 로컬을 사용해서 각각의 쓰레드마다 별도의 저장소가 부여되어 해당 쓰레드만 해당 데이터에 접근할 수 있게 하여 커넥션을 동기화 해준다.

내부 코드를 보면 자원을 맵으로 저장하고 가져오고 확인하는 로직이 있다

public static Map<Object, Object> getResourceMap() {
		Map<Object, Object> map = resources.get();
		return (map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap());
	}

	/**
	 * Check if there is a resource for the given key bound to the current thread.
	 * @param key the key to check (usually the resource factory)
	 * @return if there is a value bound to the current thread
	 * @see ResourceTransactionManager#getResourceFactory()
	 */
	public static boolean hasResource(Object key) {
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		Object value = doGetResource(actualKey);
		return (value != null);
	}

	/**
	 * Retrieve a resource for the given key that is bound to the current thread.
	 * @param key the key to check (usually the resource factory)
	 * @return a value bound to the current thread (usually the active
	 * resource object), or {@code null} if none
	 * @see ResourceTransactionManager#getResourceFactory()
	 */
	@Nullable
	public static Object getResource(Object key) {
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		return doGetResource(actualKey);
	}
...

 

 

스프링이 제공하는 트랜잭션 매니저를 다시 정리해보면 

트랜잭션 시작1

  1. 서비스 계층에서  transactionManager.getTransaction() 를 호출해서 트랜잭션 시작하면
  2. 트랜잭션 시작하기 위해서 트랜잭션 매니저가 내부에서 DataSource를 사용해 커넥션을 생성
  3. 커넥션을 수동 커밋모드로 변경해서 실제 DB트랜잭션 시작
  4. 커넥션을 트랜잭션 동기화 매니저에 보관
  5. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관! 이로써 멀티 쓰레드 환경에서 안전하게 보관할 수 있다.

트랜잭션 시작2

  1. 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다.
  2. 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요해서  DataSourceUtils.getConnection() 를 사용해 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내 사용한다. 이로써 같은 커넥션을 사용하고 트랜잭션도 유지된다.
  3. 획득한 커넥션을 사용해서 SQL을 DB에 전달해서 실행한다.
리포지토리(Repository)는 데이터베이스와 관련된 작업을 수행하는 객체로, 주로 데이터베이스와의 상호작용을 추상화하고 데이터 액세스를 담당한다.
예를 들어, 스프링 프레임워크에서는 JPA(Java Persistence API)나 Hibernate와 같은 ORM(Object-Relational Mapping) 기술을 사용할 때 리포지토리를 자주 사용한다. 이를 통해 데이터베이스와의 통신을 추상화하고, 개발자는 객체 지향적인 방식으로 데이터베이스와 상호작용할 수 있다. 

트랜잭션 종료3

  1. 비즈니스 로직 끝나고 트랜잭션을 종료한다. 트랜잭션은 커밋하거나 롤백하면 종료된다
  2. 트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다
  3. 획득한 커넥션 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
  4. 전체 리소스 정리한다.
    1. 트랜잭션 동기화 매니저 정리. 쓰레드 로컬은 사용 후 꼭 정리해야한다.
    2. 자동커밋 true로 되돌린다. 커넥션풀을 고려!
    3. close호출해 커넥션 종료! 커넥션 풀 사용했을시에는 종료하면 커넥션 풀에 반환된다.

 

 

TransactionTemplate 템플릿은

반복되는 성공시 커밋, 롤백 코드를 깔끔하게 해결하고 비즈니스 로직만 남도록 한다.

 

이 경우, 비즈니스 로직이 정상수행 되면 커밋하고

언체크 예외 발생하면 롤백한다. 그 외에 경우 커밋한다.

 

서비스 로직에 비즈니스 로직만 담고 트랜잭션 기술을 처리하는 코드는 없애기 위해서

스프링 AOP를 통해 프록시를 도입했다

프록시를 사용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.

 

스프링이 제공하는 트랜잭션 AOP기능을 사용하면 프록시를 편하게 사용할 수 있다.

 

다음 시간에 트랜잭션 AOP에 대해 공부하고 정리해보자.

728x90

+ Recent posts