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
도커데스크탑만 켜둔 상태 (참고로 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를 통해 띄운 도커 컨테이너로는 접속이 안되었던 것이다.
@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;
}
고객이 A 계좌에서 B 계좌로 일정 금액을 이체하는 트랜잭션을 고려하면, 이체 작업은 A 계좌에서의 출금과 B 계좌로의 입금 두 단계로 이루어진다. 이 두 단계는 원자성을 가져야 하며, 하나의 단계가 실패하면 다른 단계도 롤백되어야한다.
일관성 (Consistency):
예시: 주문 및 재고 관리
상품 주문 트랜잭션에서는 주문이 들어올 때마다 재고가 감소해야 한다. 이때 주문 트랜잭션이 성공하면 재고가 감소하고, 실패하면 재고는 변하지 않아야 한다. 이를 통해 데이터베이스는 일관된 상태를 유지한다.
격리성(고립성) (Isolation):
예시: 동시 계좌 이체
여러 사용자가 동시에 계좌 간 이체를 시도하는 상황에서, 각각의 트랜잭션이 다른 트랜잭션에 영향을 주지 않아야 한다. A 계좌에서의 이체 작업이 완료되기 전까지는 B 계좌에서의 이체 작업을 다른 트랜잭션이 간섭하지 않도록 보장되어야 한다.
격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
지속성 (Durability):
예시: 주문 정보의 저장
고객이 주문을 완료하면 해당 주문 정보는 데이터베이스에 영구적으로 저장되어야 한다. 시스템 장애 또는 다른 문제가 발생하더라도 주문 정보는 지속되어야 하며, 고객이 주문을 확인할 수 있어야 한다. 데이터베이스 트랜잭션의 특성을 나타내는 약어로 Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(지속성)을 나타낸다.
격리성을 완벽하게 보장하기 위해서는
트랜잭션을 순서대로 실행할 수 있지만 이렇게 동작할 경우 동시처리 성능이 나빠진다.
트랜잭션은 기본적으로 동시에 여러 개가 실행될 수 있다.
격리 수준은 이러한 동시성에서 발생할 수 있는 문제들을 어떻게 제어할 것인지를 정의한다.
즉,트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때, 특정 트랜잭션이 다른 트랜잭션에서 데이터를 변경하거나 조회할지 등 간섭할지 여부를 제어하는데 사용된다. 이처럼 격리 수준은 트랜잭션 간의 데이터 일관성을 유지하기 위해 중요하다.
<트랜잭션 격리 수준 Isolation level>
READ UNCOMMITTED (커밋되지 않은 읽기):
다른 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있다. 이로 인해 Dirty Read(더티 리드), Non-Repeatable Read(반복 불가능한 읽기) 등의 문제가 발생할 수 있다.
활용 예시: 실시간 보고서 생성 시에 최신 데이터를 빠르게 반영하고자 할 때 사용될 수 있다.
Dirty Read(더티 리드)는 다른 트랜잭션이 아직 커밋되지 않은 데이터를 읽는 현상을 말한다. 트랜잭션이 데이터를 변경하고 커밋하기 전에 다른 트랜잭션이 해당 데이터를 읽으면, 그 읽힌 데이터는 커밋되지 않아 롤백될 수 있으므로 정확하지 않거나 무의미한 데이터가 될 수 있다.
트랜잭션 격리 수준이 READ COMMITTED 이상으로 설정되면 Dirty Read 문제를 방지할 수 있다. READ COMMITTED 이상의 격리 수준에서는 다른 트랜잭션이 아직 커밋되지 않은 데이터를 읽지 못하도록 보장되기 때문이다.
READ COMMITTED (커밋된 읽기):
커밋된 데이터만 읽을 수 있다. 다른 트랜잭션이 작업을 커밋하기 전까지 데이터를 읽을 수 없어 Dirty Read는 발생하지 않지만, Non-Repeatable Read 문제가 발생할 수 있다.
활용 예시: 데이터베이스의 무결성을 중시하는 경우에 사용될 수 있습니다.
Non-Repeatable Read : 트랜잭션이 동일한 쿼리를 수행했을 때, 두 번째 읽기에서 다른 값을 반환하는 문제를 가리킨다. 이는 동일한 트랜잭션 안에서 동일한 데이터를 읽을 때 일관성이 유지되지 않는 상황을 말한다.
트랜잭션 A:
시작 시점에 데이터 X를 읽습니다. (값: 100)
트랜잭션 B:
트랜잭션 A가 진행 중일 때, 데이터 X를 수정하고 커밋합니다. (값: 200)
트랜잭션 A:
이어서 동일한 데이터 X를 다시 읽으려고 합니다.
Non-Repeatable Read 문제: 이전에 읽은 값과 다르게 새로운 값 (200)를 읽게 됩니다.
이렇게 트랜잭션이 같은 데이터를 두 번 이상 읽을 때, 중간에 다른 트랜잭션이 해당 데이터를 수정하고 커밋하면서 값이 변경되어 버리는 현상이 발생한다.이러한 상황에서는 트랜잭션 내에서 일관성이 깨지게 되어 Non-Repeatable Read 문제가 발생합니다.
트랜잭션 격리 수준이 REPEATABLE READ 이상으로 설정되면 Non-Repeatable Read 문제는 방지된다. REPEATABLE READ 이상의 격리 수준에서는 동일한 트랜잭션 안에서 같은 데이터를 여러 번 읽어도 그 값이 변경되지 않음이 보장된다.
REPEATABLE READ (반복 가능한 읽기):
동일한 쿼리를 여러 번 실행해도 항상 동일한 결과를 보장한다. 다른 트랜잭션이 해당 범위의 데이터를 변경할 수 없도록 하지만 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.
활용 예시: 일정 시간 동안 동일한 데이터를 여러 번 읽어야 하는 작업에서 사용될 수 있습니다.
REPEATABLE READ는 트랜잭션 번호를 참고하여 자신보다 먼저 실행된 트랜잭션의 데이터만을 조회한다. 만약 테이블에 자신보다 이후에 실행된 트랜잭션의 데이터가 존재한다면 언두 로그를 참고해서 데이터를 조회한다.
SERIALIZABLE (직렬화 가능):
트랜잭션 간에 완전한 격리를 보장한다. 다른 트랜잭션이 해당 범위의 데이터에 접근하지 못하도록 막아주어 가장 엄격한 격리 수준이지만, 접근하지 못해 트랜잭션이 순차적으로 실행되어야하기 때문에 동시성이 낮아질 수 있다. 가장 안전하고 가장 성능이 떨어진다.
활용 예시: 데이터 정합성이 최우선이 되는 금융 거래와 같이 매우 중요한 트랜잭션에서 사용될 수 있다.
트랜잭션이 동작하기 위해 필요한 것
0. 자동커밋이 아닌 수동 커밋이어야 한다.
자동커밋은 set autocommit true; 각각의 쿼리 실행 직후에 자동으로 커밋호출된다. 바로 데이터베이스에 반영되어 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없다.
수동 커밋 은 set autocommit false; 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다.
1. DB 서버에 연결하기 위한 커넥션이 필요하다.
커넥션을 생성해 맺고
2. DB서버는 내부 세션을 만든다.
3. 커넥션을 통한 모든 요청이 이 세션을 통해 실행하게 된다.
4. 세션은 트랜잭션을 시작하고 , 커밋 또는 롤백을 통해 트랜잭션을 종료한다.
5. 사용자가 커넥션을 닫거나 DBA(데이터 베이스 관리자)가 세션을 강제로 종료하면 세션은 종료된다.
커넥션 풀이 10개의 커넥션을 생성하면 세션도 10개 만들어진다.
커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이므로
해당 트랜잭션을 시작한 세션에서만 변경 데이터가 보이고 다른 세션에서는 변경데이터가 보이지 않는다.
커밋하기 전에 롤백을 하면 직전 상태로 복구된다.
<락 사용>
세션 1이 트랜잭션 시작하고 데이터를 수정하는 동안 아직 커밋하지 않았는데
세션2에서 동시에 같은 데이터를 수정하게 되면 문제가 발생한다.
이를 방지하기 위해 커밋이나 롤백전까지 다른 세션에서 해당 데이터를 수정할 수 없게 락 개념이 사용된다.
락을 획득해서 커밋후에 락을 반납하기까지 다른 세션에서는 락을 획득하기 위해서 대기하는 방식이다. 설정된 락 타임아웃 시간보다 더 오래 대기하면 락 타임아웃 오류가 발생한다.
조회시에 보통 락 획득하지 않고 바로 데이터를 조회할 수 있지만 조회시에도 락을 획득하고 싶다면 select for update구문을 사용하면 된다. 이렇게 되면 특정 세션이 조회할 때 락을 가져가 버리기 때문에 해당 데이터를 다른 세션에서 변경할 수 없다.
조회시점에 락 필요한 경우
예) 금액 조회하고 그 금액 정보로 다른 계산을 수행해야할 때
<트랜잭션을 어디에서 시작하고 어디에서 커밋해야하는가?>
트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야한다. 로직이 잘못되었을 시 롤백하기 위해서이다. 하지만 트랜잭션을 시작하기 위해서는 커넥션이 필요하다. 커넥션만들고 트랜잭션 커밋, 커넥션 종료까지 해야한다.
쓰레드 로컬을 사용해서 각각의 쓰레드마다 별도의 저장소가 부여되어 해당 쓰레드만 해당 데이터에 접근할 수 있게 하여 커넥션을 동기화 해준다.
내부 코드를 보면 자원을 맵으로 저장하고 가져오고 확인하는 로직이 있다
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
서비스 계층에서 transactionManager.getTransaction() 를 호출해서 트랜잭션 시작하면
트랜잭션 시작하기 위해서 트랜잭션 매니저가 내부에서 DataSource를 사용해 커넥션을 생성
커넥션을 수동 커밋모드로 변경해서 실제 DB트랜잭션 시작
커넥션을 트랜잭션 동기화 매니저에 보관
트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관! 이로써 멀티 쓰레드 환경에서 안전하게 보관할 수 있다.
트랜잭션 시작2
서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다.
리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요해서 DataSourceUtils.getConnection() 를 사용해 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내 사용한다. 이로써 같은 커넥션을 사용하고 트랜잭션도 유지된다.
획득한 커넥션을 사용해서 SQL을 DB에 전달해서 실행한다.
리포지토리(Repository)는 데이터베이스와 관련된 작업을 수행하는 객체로, 주로 데이터베이스와의 상호작용을 추상화하고 데이터 액세스를 담당한다. 예를 들어, 스프링 프레임워크에서는 JPA(Java Persistence API)나 Hibernate와 같은 ORM(Object-Relational Mapping) 기술을 사용할 때 리포지토리를 자주 사용한다. 이를 통해 데이터베이스와의 통신을 추상화하고, 개발자는 객체 지향적인 방식으로 데이터베이스와 상호작용할 수 있다.
트랜잭션 종료3
비즈니스 로직 끝나고 트랜잭션을 종료한다. 트랜잭션은 커밋하거나 롤백하면 종료된다
트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다
획득한 커넥션 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
전체 리소스 정리한다.
트랜잭션 동기화 매니저 정리. 쓰레드 로컬은 사용 후 꼭 정리해야한다.
자동커밋 true로 되돌린다. 커넥션풀을 고려!
close호출해 커넥션 종료! 커넥션 풀 사용했을시에는 종료하면 커넥션 풀에 반환된다.
TransactionTemplate 템플릿은
반복되는 성공시 커밋, 롤백 코드를 깔끔하게 해결하고 비즈니스 로직만 남도록 한다.
이 경우, 비즈니스 로직이 정상수행 되면 커밋하고
언체크 예외 발생하면 롤백한다. 그 외에 경우 커밋한다.
서비스 로직에 비즈니스 로직만 담고 트랜잭션 기술을 처리하는 코드는 없애기 위해서
스프링 AOP를 통해 프록시를 도입했다
프록시를 사용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.