스프링 DB 1(김영한 강의) - 섹션 6 : 스프링과 문제 해결(예외처리, 반복)

섹션 목적 : 남은 예외 누수와 JDBC 반복 문제를 스프링을 통해 해결해보자

 

1. 체크예외와 인터페이스

   (1) 서비스 계층은 가급적 특정 기술에 의존하지 않고 순수하게 유지하는 것이 좋다

   (2) 이렇게 하려면, 서비스에서 SQLException에 대한 의존성을 제거해야 함. 어떻게 해야할까?

   (3) Repository가 던지는 SQLException 체크 예외를 런타임 예외로 바꿔서 서비스에 던지자

   (4) 이렇게 하면 서비스는 해당 예외를 무시할 수 있고 예외에 대한 의존도를 제거할 수 있다

 

2. 인터페이스 도입

   (1) MemberRepository라는 인터페이스를 만들어서 repository를 구현기술에 따라 쉽게 변경할 수 있도록 하자

   (2) 이렇게 하면 service는 MemberRepository 인터페이스에만 의존하면 되므로, MemberRepository의 구현기술이 변경되더라도 service는 코드를 그대로 유지할 수 있다

   (3) 그런데 만약 인터페이스 구현체에 체크 예외를 사용하려면 인터페이스부터 체크 예외가 선언되어 있어야함

   (4) 인터페이스부터 해당 예외에 종속적이게 되므로 인터페이스가 오염됨

   (5) 따라서, 런타임 예외로 바꾸자

 

3. 런타임 예외 적용

   (1) MemberRepository 인터페이스를 상속받아서, SQLException을 던지는 코드를 런타임 예외인 MyDbException으로 감싸서 밖으로 던지도록 바꾼다

   (2) 이렇게 하면 서비스는 이제 SQLException이 제거되어 의존하지 않고, MemberRepository 인터페이스에만 의존하게 됨

   (3) 체크 예외를 언체크 예외로 변경해서 다음과 같은 이점이 생겼다

      ① 인터페이스와 서비스에 순수성을 유지할 수 있게 되었음

      ② 데이터 접근 계층이 JDBC에서 JPA로 변경되더라도 서비스 계층의 코드는 변경할 필요가 없음

   (4) 그런데, 어떨 때는 서비스가 리포지토리에서 넘어오는 특정 예외에 대해 복구를 시도할 수 있다

   (5) 지금 방식은 항상 MyDbException만 넘어와서 예외를 구분할 수 없다

   (6) 그럼 어떻게 예외를 구분할 수 있을까?

 

4. 데이터 접근 예외 직접 만들기

   (1) 데이터를 DB에 저장할 때 같은 id가 DB에 있다면, DB는 오류코드를 반환하고 JDBC 드라이버는 해당 오류코드를 감싸서 SQLException을 던진다

   (2) 그래서 SQLException에는 DB가 제공하는 errorCode가 있음

   (3) 예를 들어 H2에서 키 중복 오류 에러코드는 23505이다(e.getErrorCode()를 통해 확인할 수 있음)

   (4) 이를 활용해서 만약 errorCode가 23505일 경우 리포지토리는 throw new MyDuplicateKeyException(e)를 던지도록 구현한다

   (5) 남은 문제

      ① errorCode는 DB마다 달라서 DB가 변경될 때마다 errorCode도 모두 변경해야함

      ② DB가 전달하는 오류는 수천가지가 있는데, 이를 다 런타임 예외로 재정의해서 바꿔야함

 

5. 스프링 예외 추상화 이해

   (1) 스프링은 그래서 데이터 접근 계층에 대한 수십개의 예외를 정리해서 제공함. 특정 기술에 종속적이지 않음

   (2) 그래서 서비스에서도 스프링이 제공하는 이 예외를 사용하면 됨

   (3) 스프링은 또한 DB별로 발생하는 오류코드에 따라 스프링이 정의한 예외로 변환해주는 변환기도 제공함

   (4) 따라서, 우리는 단지 exTranslator.translate("save", sql, e)를 사용하면 예외를 쉽게 변환할 수 있다

 

6. JDBC 반복문제 해결 - JDBC Template

   (1) 서비스는 순수해짐. 이제 repository의 반복 문제를 해결해보자

   (2) JDBC 반복 패턴

      ① 커넥션 조회, 동기화

      ② PreparedStatement 생성 및 파라미터 바인딩

      ③ 쿼리 실행

      ④ 결과 바인딩

      ⑤ 예외 발생시 예외 변환기 실행

      ⑥ 리소스 종료

   (3) 리포지토리의 메소드들은 모두 위와 같은 형태를 띄고 있음

   (4) 이런 반복을 템플릿 콜백패턴인 JDBC Template을 사용해서 해결함

   (5) JDBC Template은 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기까지 자동으로 실행해줌


이펙티브 자바 - 아이템 7 : 다 쓴 객체 참조를 해제하라

- 스택을 배열로 구현한 코드를 예시로 보자

- pop() 메소드에서, size만 줄여줄 뿐 배열에 담겨있는 객체를 처리하지 않는다

- 이렇게 되면 이 배열에 쌓이는 객체는 가비지 컬렉터가 회수해가지 않기 때문에, 메모리 누수가 일어난다

- 따라서, 해당 참조를 다 썼을 때 null처리를 하면 된다

- pop() 메소드에서 element[size] = null 로 하면 메모리 누수가 발생하지 않는다

- 가장 좋은 참조 해제 방법은 참조를 담은 변수를 유효범위 밖으로 밀어내는 것이다

- null 처리는 자기 메모리를 직접 관리하는 클래일 때 사용한다


코테준비

1. 프로그래머스 - N으로 표현

- bottom-up 방식으로 주어진 숫자를 사용해가며 가장 최소의 경우를 구한다

 

2. 프로그래머스 - 정수 삼각형

- DP는 결국 완전탐색에서 중복된 계산을 한번 더 하지 않는 것일 뿐이다

- 주어진 문제를 완전탐색으로 먼저 풀고 중복된 계산을 cache배열을 이용해서 줄이자

 

3. 프로그래머스 - 사칙연산

- 완전 탐색으로 먼저 풀고 중복된 계산을 줄이는 방법으로 풀었다


오늘 하루도 고생했다!!! 계속 꾸준하게 나아가자

'TIL(Today I Learned)' 카테고리의 다른 글

2023.07.04  (0) 2023.07.04
2023.07.03  (0) 2023.07.03
2023.06.29  (0) 2023.06.30
2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
스프링 DB 1(김영한 강의) - 섹션 5 : 자바 예외 이해

섹션 목적 : 자바의 예외에 대해 이해해보자

 

1. 예외 계층

   (1) Object - Throwable - Error, Exception

   (2) Error : 메모리 부족, 시스템 오류와 같이 복구 불가능한 예외. 개발자는 잡으려하면 안됨

   (3) Exception - ..., RuntimeException

   (4) Exception과 그 하위 예외(RuntimeException 제외) -> 체크 예외

   (5) RuntimeException과 그 하위 예외 -> 언체크 예외

 

2. 예외 기본 규칙

   (1) 규칙

      ① 예외는 잡아서 처리하거나, 밖으로 던져야한다(자기를 호출한 상위 메소드로)

      ② 예외를 잡거나 던질 때, 그 예외의 자식들도 함께 처리된다

   (2) 예외를 처리하지 못하고 계속 던지면?

      ① 자바 main()의 경우, 예외 로그를 출력하고 종료

      ② 웹 애플리케이션의 경우 시스템이 종료되면 안됨. 따라서 WAS가 예외를 받아서 처리함. 사용자에게 개발자가 지정한 예외 페이지를 보여줌

 

3. 체크 예외 기본 이해

   (1) Exception과 그 하위 예외(RuntimeException 제외)는 컴파일러가 체크하는 체크 예외

   (2) 체크 예외는 처리하거나 밖으로 던지도록 선언해야함. '체크 예외를 어떻게 하겠다'라는 것을 명시적으로 보여줘야함. 그렇지 않으면 컴파일 오류 발생

   (3) 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치

   (4) 단점 : 개발자가 신경쓰고 싶지 않은 체크 예외를 잡거나 던져야하기 때문에 너무 번거로움. 또한 의존관계에 따른 단점도 존재함

 

4. 언체크 예외 기본 이해

   (1) RuntimeException과 그 하위 예외는 컴파일러가 체크하지 않는 언체크 예외

   (2) 언체크 예외는 꼭 잡아서 처리하거나 던지는 것을 명시적으로 표현할 필요가 없음. 처리하지 않으면 자동적으로 던짐

   (3) 언체크 예외는 throws를 선언해도 됨. 중요한 예외의 경우에 이렇게 선언해두면, 해당코드를 호출하는 개발자가 IDE를 통해 볼 수 있음

   (4) 장점 : 신경쓰고 싶지 않은 예외를 무시할 수 있음. 신경쓰고 싶지 않은 예외의 의존관계를 참조하지 않아도 됨

   (5) 단점 : 개발자가 실수로 예외를 누락할 수 있음

   (6) 따라서, 체크 예외와 언체크 예외의 차이는 예외를 처리하지 않을 때 밖으로 던지는 부분에 있음. 체크는 명시적으로 throws를 해줘야하는 반면, 언체크는 생략해도 됨

 

5. 체크 예외 활용

   (1) 그렇다면 언제 체크 or 언체크를 사용해야할까?

   (2) 원칙

      ① 기본적으로 언체크를 활용하자

      ② 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에 사용하자. 개발자가 실수로 이 예외를 놓칠 수 없게끔 만들 때 사용

   (3) 체크 예외 문제점

      ① Repository에서 발생한 Exception이 해결할 수 없는 Service와 Controller까지 올라오면서 신경쓰고 싶지 않은 예외를 처리해야함

      ② 컨트롤러나 서비스는 throws Exception을 통해 예외에 의존하게 됨. 만약 Repository가 JDBC에서 JPA로 변경돼서 JPA Exception이 발생할 경우, 서비스와 컨트롤러 모두 코드를 변경해야하는 문제가 발생함

   (4) 그렇다고 체크 예외를 공통으로 처리하기 위해 throws Exception을 사용하지 말자

 

6. 언체크 예외 활용

   (1) SQLException을 RuntimeSQLExcption으로 변경. Service와 Controller는 해당 예외에 의존할 필요가 없음

   (2) 서비스나 컨트롤러는 이런 복구 불가능한 예외를 신경쓸 필요가 없음

   (3) 또한, 서비스나 컨트롤러는 해당 예외에 의존할 필요가 없음

   (4) 이렇게 런타임 예외로 바꾸게 되면

      ① 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러나 서비스는 코드를 변경할 필요가 없음

      ② 해당 기술에 의존하는 Repository와 ControllerAdvice만 변경하면 됨

 

7. 예외 포함과 스택 트레이스

   (1) 예외를 전활할 때는 기존 예외를 생성자 안에 감싸서 전환해야 함

   (2) 그래야 기존에 발생한 예외으 스택 트레이스를 확인할 수 있음


이펙티브 자바 - 아이템 6 : 불필요한 객체 생성을 피하라

- 똑같은 기능의 객체를 매번 생성하는 것보다 객체 하나를 재사용하는 것이 좋다

- 예를 들어서, String s = new String("bikini")를 반복문에서 사용할 경우, 계속해서 새로운 인스턴스가 만들어지게 된다

- 따라서, String s = "bikini"를 사용함으로써 같은 문자열 객체가 재사용되게 해야한다

- 정적 팩토리 메소드를 사용하면 불필요한 객체 생성을 막을 수 있다(ex. Boolean.valueOf(String) -> 항상 같은 Boolean 객체가 반환된다)

- 생성비용이 비싼 경우 미리 클래스 초기화 과정에서 생성해두고 이 인스턴스를 재사용하는 방법으로 사용한다

- 박싱된 기본 타입보다는 기본타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의해야 한다

- 예를 들어 Long sum = 0L에서 sum에 계속 숫자를 더하면, Long은 불필요한 객체가 무수히 만들어지는 경우가 발생한다


코테준비

1. 프로그래머스 - k번째 수

- stream을 이용해서 배열을 subarray할 수 있다는 것을 깨달았음


오늘 하루도 고생했고, 다음주 토요일에 토스 코테 한번 봐보자!!!

'TIL(Today I Learned)' 카테고리의 다른 글

2023.07.03  (0) 2023.07.03
2023.06.30  (0) 2023.06.30
2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
2023.06.26  (0) 2023.06.26
스프링 DB 1(김영한 강의) - 섹션 4 : 스프링과 문제해결, 트랜잭션

섹션 목적 : 스프링이 트랜잭션을 사용할 때 발생하는 문제점을 어떻게 해결하는지 알아보자

 

1. 문제점들

   (1) 애플리케이션 구성은 Controller(프레젠테이션) -> Service(비즈니스 로직) -> Repository(데이터 접근계층) -> DB 

   (2) 프레젠테이션, 데이터 접근 계층은 다른 기술로 변경해도 비즈니스 로직(Service)는 최대한 변경없이 유지해야 함

   (3) 이렇게 하려면 서비스 계층이 특정 기술에 종속적이지 않게 개발해야 함

   (4) 지금까지 개발한 MemberServiceV2의 문제점

      ① 트랜잭션을 사용하기 위해서 JDBC 기술에 의존함

         * JDBC에서 JPA로 변경할 경우, 모든 코드를 고쳐야함

         * 비즈니스 로직보다 트랜잭션 처리 코드가 더 많음

      ② 예외 누수(ex. SQL Exception)

      ③ JDBC 반복 문제(ex. repository의 try, catch, finally 반복)

   (5) Spring을 이용해서 위 3가지 문제를 해결해보자, 이번 섹션에서는 ① 문제를 해결해보자

 

2. 트랜잭션 추상화

   (1) 구현 기술에 따라서 트랜잭션을 처리하는 방법이 다름

      ① JDBC : con.setAutoCommit(false)

      ② JPA : transaction.begin()

   (2) 만약 JDBC에서 JPA로 변경하면 서비스의 트랜잭션 처리 코드를 모두 변경해야함

   (3) 따라서, 스프링은 트랜잭션을 처리하는 기술을 추상화해서 PlatformTransactionManager라는 트랜잭션 매니저를 제공함

   (4) 우리는 이제 트랜잭션을 사용할 때, txManager.getConnection() -> 비즈니스 로직 -> txManager.commit() or rollback()을 사용하기만 하면되고, JDBC를 쓰다가 JPA로 변경해도 트랜잭션 처리 코드를 변경할 필요가 없음

 

3. 트랜잭션 동기화

   (1) 트랜잭션 매니저는 크게 두가지 역할을 함

      ① 트랜잭션 추상화

      ② 리소스 동기화

   (2) 리소스 동기화란

      ① 트랜잭션을 유지하려면, 트랜잭션의 시작부터 끝까지 같은 DB 커넥션을 유지해야함

      ② 이전에 우리는 커넥션 동기화를 위해서, 커넥션을 파라미터로 전달하는 방법을 사용함

      ③ 이렇게 되면 커넥션을 매번 파라미터로 넘겨야해서 코드가 지저분해짐

      ④ 그래서 스프링은 트랜잭션 동기화 매니저를 제공함

   (3) 트랜잭션 매니저 내부에서 트랜잭션 동기화 매니저를 사용하고, 트랜잭션 동기화 매니저는 멀티 쓰레드 상황에서 안전하게 커넥션을 동기화 해줌

   (4) 따라서, 커넥션이 필요하면 동기화 매니저를 통해서 커넥션을 획득하면 됨

   (5) 그러므로, 커넥션을 파라미터로 전달할 필요가 없음

   (6) 트랜잭션 동작 방식

      ① 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 생성

      ② 이 커넥션은 트랜잭션 동기화 매니저에 보관

      ③ 리포지토리는 동기화 매니저에 등록된 커넥션을 꺼내서 사용

      ④ 트랜잭션이 종료되면, 트랜잭션 매니저는 동기화 매니저에 등록된 커넥션을 닫고 종료

 

4. 트랜잭션 문제 해결 - 트랜잭션 매니저 1

   (1) 트랜잭션 동기화 매니저가 관리하는 커넥션을 사용하려면, repository는 커넥션을 가져올 때, DataSourceUtils.getConnection(dataSource)를 사용해야 함

   (2) 또한, 트랜잭션 중간에 커넥션을 닫으면 안되므로, DataSourceUtils.releaseConnection()을 사용해야 함

   (3) Service는

      ① txManager.getConnection() 으로 트랜잭션 시작

      ② 비즈니스 로직

      ③ txManager.commit() or rollback() 으로 트랜잭션 커밋 or 롤백 후 종료

 

5. 트랜잭션 문제 해결 - 트랜잭션 매니저 2

   (1) 트랜잭션 전체 흐름 정리

      ① 서비스에서 txManager.getTransaction() 으로 트랜잭션 시작

      ② txManager는 DataSource를 통해 DB 커넥션을 생성

      ③ 생성된 커넥션을 setAutoCommit(false)로 변경

      ④ 이 커넥션을 트랜잭션 동기화 매니저에 보관

      ⑤ 비즈니스 로직을 시작하면서 리포지토리 메소드들을 호출

      ⑥ 리포지토리 메서드들은 트랜잭션에서 시작된 커넥션이 필요하므로, DataSourceUtils.getConnection()을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용함 -> 같은 커넥션을 사용

      ⑦ 획득한 커넥션으로 DB에 SQL을 전달

      ⑧ 비즈니스 로직이 끝나면, 해당 트랜잭션을 커밋 or 롤백 후 종료

      ⑨ 트랜잭션 매니저는 사용한 커넥션을 정리함

   (2) 트랜잭션 추상화 덕분에 JDBC가 JPA로 변경되어도 서비스 코드를 유지할 수 있게 되었음

 

6. 트랜잭션 문제 해결 - 트랜잭션 템플릿

   (1) 트랜잭션을 사용하는 코드를 보면

      ① 트랜잭션 시작

      ② 비즈니스 로직

      ③ 커밋 or 롤백

이 반복되는 것을 볼 수 있음

   (2) 이런 형태가 계속 반복되므로, 템플릿 콜백 패턴을 통해 이런 반복문제를 해결할 수 있음

   (3) MemberService에 txTemplate.execute( ... 비즈니스 로직 ... ) 을 사용

   (4) 트랜잭션 템플릿으로 트랜잭션 사용 시 반복되는 코드를 제거할 수 있음

   (5) 하지만, 여전히 Service에 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 들어가 있음

   (6) 어떻게 하면 Service 계층을 순수하게 유지할 수 있을까?

 

7. 트랜잭션 문제 해결 - 트랜잭션 AOP 이해

   (1) 스프링 AOP를 통해 프록시를 도입해서 문제를 해결해보자

   (2) @Transactional을 사용해서 스프링이 제공하는 AOP 기능을 사용하면 Service의 프록시 객체가 생성됨

   (3) 트랜잭션 AOP는 PlatformTransactionManager를 이용해서 만들고 스프링이 프록시 객체를 만들기 때문에, 스프링 빈으로 등록해야 함

   (4) 이제 순수한 비즈니스 로직만 남길 수 있음

 

8. 스프링 부트의 자동 리소스 등록

   (1) 스프링 부트는 DataSource와 트랜잭션 매니저를 자동으로 등록해줌

   (2) 우리는 application.properties에 url, username, password를 쓰면, 스프링 부트는 기본적으로 Hikari 데이터소스를 만들고, 이를 이용해서 트랜잭션 매니저를 생성해서 스프링 빈으로 등록함


이펙티브 자바 - 아이템 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

- 많은 클래스는 하나 이상의 다른 자원(클래스)에 의존함

- 예를 들어 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스 또는 싱글턴으로 구현한 경우가 있음

- 하지만, 두 방식 모두 단 하나의 사전만 사용할 수 있음. 실전에서는 사전이 언어 별로 있고, 특수 어휘용 사전 등 다양한 사전이 적용될 수 있음

- 따라서, 사용하는 사전에 따라 동작이 달라지는 맞춤법 검사기를 제공해야 함

- 이렇게 사용하는 자원에 따라 동작이 동적으로 달라지는 클래스는 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이 적절함

- 생성자에 넘겨줄 수도 있고, Supplier<T>를 넘겨주는 방법도 있음


코테준비

한 두문제 푼거같은데... 정리할게...!!! 지금 생각나는건 Stream을 더 익혀보자


오늘 하루도 고생했다!!!

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.30  (0) 2023.06.30
2023.06.29  (0) 2023.06.30
2023.06.27  (0) 2023.06.27
2023.06.26  (0) 2023.06.26
2023.06.23  (0) 2023.06.23
스프링 DB 1(김영한 강의) - 섹션 3 : 트랜잭션 이해

섹션 목적 : 트랜잭션을 이해해보자

 

1. 트랜잭션 - 개념이해

   (1) A -> B에게 5000원을 계좌이체를 가정. 이후에 A = A - 5000, B = B + 5000이 되어야함

   (2) 그런데, A - 5000후에 시스템에 문제가 발생하면, A에 5000원만 빠지는 심각한 문제가 발생

   (3) 따라서, 위 두 행위는 하나의 작업 처럼 수행돼야함

   (4) 트랜잭션은 이러한 하나의 작업 단위임. 하나라도 실패하면 rollback, 전부 성공하면 DB에 commit

   (5) 트랜잭션의 ACID

      ① A(원자성) : 트랜잭션 내의 작업들은 마치 하나의 작업(원자)처럼 모두 성공 or 실패해야함

      ② C(일관성) : 트랜잭션 이전과 이후 DB의 상태는 유효해야함. 트랜잭션 이후에도 DB의 제약이나 규칙이 만족해야함

      ③ I(격리성) : 트랜잭션은 다른 트랜잭션으로부터 독립되어야함. 여러 트랜잭션이 동시에 수행되더라도, 마치 혼자 수행하는 것처럼 동작해야 함

      ④ D(지속성) : 트랜잭션을 성공적으로 끝내면 그 결과는 항상 기록되어야함. 중간에 문제가 발생하더라도, 로그 등을 통해서 성공한 트랜잭션을 복구해야함

 

2. DB 연결구조와 DB 세션

   (1) 클라이언트는 DB와 연결을 요청해서 커넥션을 맺으면, DB는 이 커넥션과 연결된 세션을 만듬

   (2) 앞으로 해당 커넥션으로 요청이 들어오면, 이 세션이 일을 처리함

   (3) 세션이 트랜잭션을 시작 -> 커밋 or 롤백 -> 트랜잭션 종료를 수행

 

3. DB 락 - 개념이해

   (1) 세션 1이 데이터를 수정하는 동안 세션 2에서 동시에 같은 데이터를 수정하려할 때 문제가 발생

   (2) 트랜잭션이 시작되서 데이터를 수정하고 커밋 or 롤백 전에, 다른 트랜잭션에서 같은 데이터를 수정하지 못하도록 하는 것이 'DB 락' 임

   (3) 다른 트랜잭션은 락을 가지고 있는 기존 트랜잭션이 커밋 or 롤백을 해서 락을 반환할 때까지 대기

   (4) 무한정 대기하는 것은 아니고, 일정 시간 동안. 이러한 락 타임아웃을 설정할 수 있음

 

4. DB 락 - 조회

   (1) 일반적인 조회 시 락을 사용하지 않음

   (2) 그렇지만, 조회할 때도 락을 획득하고 싶을 때가 있음(ex. 정산 시, 아무도 데이터에 손을 못대고 하고 싶을 때)

   (3) 이 때는 select for update 문을 사용하면, 조회에도 락을 획득할 수 있음

 

5. 트랜잭션 - 적용 1

   (1) 트랜잭션 없이 단순히 계좌이체 비즈니스 로직을 구현(MemberServiceV1)

   (2) MemberServiceV1은 이체 중 예외가 발생하면, memberA의 돈만 감소하는 문제가 발생

 

6. 트랜잭션 - 적용 2

   (1) 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 함

   (2) 이유는 비즈니스 로직이 잘못되면, 해당 비즈니스 로직 작업 전체를 rollback해야하기 때문

   (3) 그래서 service에서 커넥션을 얻어오고 -> 비즈니스 로직 실행 -> 커밋 or 롤백 -> 커넥션 종료해야 함

   (4) 그리고 트랜잭션을 사용하는 동안에는 모두 같은 커넥션을 유지해서 사용해야 함

   (5) 따라서, MemberRepositoryV2의 메소드는 service에서 생성된 Connection을 파라미터로 받아서 이 커넥션으로 작업을 수행해야 함

   (6) MemberServiceV2는

      ① dataSource.getConnection()을 통해서 Connection을 가져오고

      ② connection.setAutoCommit(false)로 트랜잭션 시작

      ③ 이 커넥션을 파라미터로 주면서 비즈니스 로직 수행

      ④ 커밋 or 롤백

      ⑤ 커넥션 종료 순서로 흘러감

   (7) 테스트를 해보면 정상적으로 동작

   (8) 하지만, 서비스 계층에 DB관련 코드가 많아지면서 지저분해지고 복잡해지는 문제 발생. 따라서, 스프링이 이를 어떻게 도와주는지 다음시간에 공부


이펙티브 자바 - 아이템 4 : 인스턴스화를 막으려거든 private 생성자를 사용하라

- 단순히 static 메서드와 static 필드만을 담은 class를 만들고 싶을 때가 있음

- 이러한 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계된 것이 아님

- 따라서, 인스턴스화를 막아야함

- 그 방법 중 클래스를 추상클래스로 만드는 방법이 있는데, 이는 인스턴스화를 막을 수 없음

- 이유는 이 클래스를 상속해서 자식 클래스를 통해 인스턴스를 만들 수 있기 때문임

- 그리고, 사용자는 abstract을 보고 '상속해서 사용하라는 건가?'라고 착각할 수 있음

- 따라서, private 생성자를 사용하면 문제를 해결할 수 있음

- private 생성자를 사용하면, 사용자가 보기에 직관성이 떨어지므로 앞에다 '인스턴스화 방지용' 주석 처리

- private 생성자는 또한 상속이 불가능함. 자식에서 spuer()을 할 수 없기 때문


코테준비

1. 프로그래머스 - 피로도

- 모든 던전을 다 탐색해보는 완전탐색 문제

 

2. 프로그래머스 - 전력망을 둘로 나누기

- 처음에는 모든 전력망을 한번씩 끊어보면서, dfs를 돌려서 찾았음

- 이 후, 다른 사람의 풀이를 보고 해당 노드의 자식 갯수를 구해서 풀 수 있는 방법으로 바꿈(트리의 성질을 이용)

 

3. 프로그래머스 - 전화번호 목록

- 전화번호를 HashSet에 넣고, 각 전화번호의 접두사를 잘라서 HashSet에 있는지 확인하는 방법으로 풀이

 

4. 프로그래머스 - 의상

- 의상의 종류에 따라 HashMap에 담고, 모든 가짓수를 수학적으로 계산해서 풀이


오늘 하루 고생했다~

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.29  (0) 2023.06.30
2023.06.28  (0) 2023.06.29
2023.06.26  (0) 2023.06.26
2023.06.23  (0) 2023.06.23
2023.06.22  (0) 2023.06.22
스프링 DB 1(김영한 강의) - 섹션 2 : 커넥션 풀과 데이터소스 이해

섹션 목적 : 커넥션 풀과 데이터소스를 이해해보자

 

1. 커넥션 풀의 등장 배경

   (1) DriverManager를 통해 DB 커넥션을 획득하는 과정은 다음과 같다

      ① DB와 TCP/IP를 통해 연결

      ② DB에 ID, PASSWORD 등 부가정보 전달

      ③ 커넥션 획득

   (2) 위와 같이 복잡하고 긴 과정을 거침. SQL 실행시간 뿐만 아니라 커넥션을 생성하는 시간도 추가되기 때문에 응답 속도에 영향을 미침

   (3) 이러한 문제를 해결하기 위해서 '커넥션 풀' 등장

 

2. 커넥션 풀 이해

   (1) 커넥션 풀이란 DB 커넥션을 여러개 생성해서 Pool에 두고 관리하는 방식임(커넥션 수영장)

   (2) 커넥션 풀 안에 있는 커넥션은 TCP/IP로 미리 DB와 연결되어있는 상태이므로, 언제든지 즉시 SQL을 DB로 전달할 수 있음

   (3) 그래서, 애플리케이션 로직은 매번 새로운 커넥션을 획득하는 것이 아니라, 커넥션 풀에 있는 커넥션을 가져다가 사용하고 반납함

   (4) 커넥션 풀은 대부분 Hikari를 사용

 

3. DataSource 이해

   (1) 커넥션을 획득하는 방법은 살펴본 것처럼, DriverManager을 사용하거나 커넥션 풀을 사용하는 등 여러 방법이 존재함

   (2) 그런데, 우리가 개발할 때 DriverManager를 이용해서 커넥션을 획득하다가 커넥션 풀로 변경하게 되면, 기존 코드를 모두 변경해야하는 문제점이 발생함

   (3) 따라서, 이러한 문제를 해결하기 위해서 'DB 커넥션을 획득하는 방법을 추상화'한 DataSource가 등장함

   (4) 사용방법 : Connection connection = dataSource.getConnection() 

   (5) DataSource의 등장으로 DriverManager에서 Hikari로 변경할 때, 애플리케이션 코드를 모두 변경할 필요 없이, DataSource의 구현체만 갈아끼우면 됨

 

4. DataSource 적용

   (1) MemberRepositoryV1에서는 DataSource를 주입받는 것으로 변경

   (2) MemberRepositoryV1의 getConnection 메서드를 dataSource.getConnection()으로 변경

   (3) close 메소드는 JdbcUtils.closeResultSet, closeStatement, closeConnection으로 변경

   (4) Test에서는 DataSource의 구현체를 DriverManagerDataSource나 HikariDataSource로 갈아끼움

   (5) DriverManagerDataSource는 항상 새로운 커넥션을 생성하는 반면, HikariDataSource는 미리 만들어 놓은 커넥션들을 사용하므로 사용하고 반환되었다면 같은 커넥션을 계속해서 사용함


이펙티브 자바 - 아이템 3 : private 생성자나 열거 타입으로 싱글턴임을 보증하라

1. 싱글턴이란?

   : 인스턴스를 오직 하나만 생성할 수 있는 클래스

 

2. 싱글턴을 만드는 방법

   (1) private 생성자, public static final Elvis INSTANCE = new Elvis();

   (2) private 생성자, private static final Elvis INSTANCE = new Elvis();, public static Elvis getInstance() { return INSTANCE; }

   (3) public enum Elvis3 {  INSTANCE; public void leaveTheBuilding() { //... } }

   (4) 중요한 포인트 : private 생성자 또는 Enum 타입으로 싱글턴임을 보장하라!


코테준비

1. 프로그래머스 - 모의고사

- 패턴에 맞게 점수를 체크하고, 점수가 최대인 사람을 반환

- for문을 이용한 완전탐색

 

2. 프로그래머스 - 소수 찾기

- 주어진 종이로 모든 수를 만들어보고, 그 중 소수의 갯수를 찾기

- 재귀를 이용해서 모든 수를 만들어보고, 소수 판별 알고리즘으로 소수를 판별한 후, Set을 이용해서 중복을 없앤 후에 갯수를 반환

 

3. 프로그래머스 - 카펫

- 모든 카펫의 경우를 해보며 계산

- 간단한 수학문제


이펙티브 자바에서 싱글턴을 만드는 방법을 소개하고 각각의 장단점을 소개했으나, 이해하지 못했다ㅜㅜ

모두 이해하지 못하더라도 핵심을 파악하여 하나씩 이해해보자

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
2023.06.23  (0) 2023.06.23
2023.06.22  (0) 2023.06.22
2023.06.21  (0) 2023.06.21
스프링 DB 1(김영한 강의) - 섹션 1 : JDBC 이해

섹션 목적 : JDBC를 이해해보자

 

1. JDBC는 어떻게 등장하게 되었는가?

   (1) 클라이언트 -> 서버 -> DB, 이 흐름에서 서버가 DB에 데이터를 저장하거나 조회함

   (2) 그래서 서버는 

      ① DB와 커넥션을 연결

      ② SQL을 전달

      ③ 결과를 응답

   하는 흐름을 가짐

   (3) 그런데 DB마다 ①, ②, ③ 방식이 모두 다르다보니, DB를 변경하면 방법을 학습해야하고 코드를 바꿔야함

   (4) 이러한 문제를 해결하기 위해 JDBC가 등장

 

2. JDBC

   (1) JDBC는 위에서 발생한 3가지 문제를 interface로 제공함

   (2) 그래서 각 DB들은 이 interface를 구현해서 드라이버(ex. MySQL JDBC Driver, Oracle JDBC Driver)로 제공하고, 개발자는 이 interface에 의존해서 개발하면 됨

   (3) JDBC 등장으로 DB가 바뀌더라도, 새로 학습하거나 코드를 바꿔야할 필요가 없어짐

   (4) 하지만, DB마다 기본적으로 SQL, 일부 사용법들이 다르기 때문에 이 부분은 변경해야함

   (5) JPA는 이런 부분도 많이 해결할 수 있도록 도와줌

 

3. JDBC와 최신 데이터 접근 기술

   (1) JDBC는 오래된 기술이어서 사용방법이 복잡함. 그래서 이러한 JDBC를 편리하게 사용할 수 있도록 도와주는 기술이 등장함. 대표적으로 SQL Mapper 기술과 ORM 기술이 있음

   (2) SQL Mapper(ex. JDBC Template, MyBatis)

      ① SQL 응답결과를 객체로 편리하게 변환해줌

   (3) ORM(ex. JPA)

      ① 객체를 관계형 DB 테이블과 매핑해주는 기술임

   (4) 하지만 이 기술 모두 JDBC를 편리하게 사용할 수 있도록 도와주는 기술임. 내부적으로 모두 JDBC를 사용한다는 뜻

 

4. 애플리케이션과 DB를 연결

   (1) Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD)

   (2) Connection은 JDBC 표준 커넥션 인터페이스임

   (3) DriverManager.getConnection(...)은 H2 JDBC Driver가 제공하는 H2 전용 커넥션임(현재 H2를 사용하고 있으므로)

 

5. JDBC DriverManager 이해

   (1) DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리함(말 그대로)

   (2) DriverManager는 라이브러리에 등록된 DB 드라이버들에게 (URL, USERNAME, PASSWORD)를 넘겨서 커넥션을 획득할 수 있는지 확인하고, 그 DB 드라이버에서 커넥션을 획득함

 

6. JDBC를 이용해서 CRUD 구현

   (1) DB 커넥션을 가지고 온다 - Connection

   (2) 커넥션에 SQL을 세팅한다 - Statement

   (3) DB에 커넥션을 전달하고 결과를 가져온다 - ResultSet

   (4) 사용한 자원(Connection, Statement, ResultSet)은 닫아준다


이펙티브 자바 - 아이템 2 : 생성자에 매개변수가 많다면 빌더를 고려하라

1. 생성자에 매개변수가 많은 경우 보통 점층적 생성자 패턴을 이용한다

   (1) 그렇지만, 이렇게 될 경우 매개변수가 많아지면 두 가지 문제점이 발생함

      ① 코드를 작성하거나 읽기가 매울 어려워짐

      ② 매개변수의 순서를 바꿔 입력해도 컴파일러는 알아채지 못하고 버그가 발생

 

2. 이 때 사용하기 좋은 방법은 빌더 패턴

   (1) 해당 class에 static class로 Builder를 만들고, 이 빌더에 값을 세팅하는 형식으로 바꿈

   (2) 사용방법은 NutritionFacts cocacola = new NutritionFacts.Builder(240, 8).calories(100).sodium(25).build(); 와 같음

   (3) 이렇게 하면 위의 두 가지 문제점을 해결할 수 있음

 

3. 또한 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋음

   (1) 예를 들어 Pizza 추상 클래스를 상속한 NewYorkPizza와 CalzonePizza가 있음

   (2) Pizza.Builder를 상속해서 각각 피자에 Builder를 만드는 방식임


스프링 DB1을 다시 보면서, 어떻게 스프링에서 DB와 연결짓는지 확실하게 구조를 이해해보자.

이펙티브 자바 예제 코드 꼭 직접 작성해보자. 백문이 불여일타!!!

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
2023.06.26  (0) 2023.06.26
2023.06.22  (0) 2023.06.22
2023.06.21  (0) 2023.06.21
스프링 DB 2(김영한 강의) - 섹션 : 스프링 전파2

섹션 목적 : 실제 예제를 통해 어떻게 스프링 전파가 활용되는지 알아보자

 

1. 세팅

   (1) 요구사항 : 회원등록시, 회원 저장 및 DB에 로그를 남기는 것

   (2) 새롭게 알게된 점 

      ① JPA의 구현체인 하이버네이트가 테스트에서 테이블을 자동으로 생성

      ② 별도의 설정이 없을 경우, Test에서는 메모리 DB로 동작

      ③ 메모리 DB 생성 -> 모든 테스트 동작 -> 메모리 DB 삭제

      ④ JPA를 통해서 데이터를 변경할 경우 Transaction이 필요. 데이터 조회시 Transaction이 불필요

   (3) 구조 : memberService 안에서 memberRepository.save()와 logRepository.save() 가 동작함

 

2. memberRepo와 logRepo에는 @Transactional이 있고, memberService에는 없는 경우, 커밋 or 롤백

   (1) memberRepo에서 트랜잭션 하나 가져와서, 커밋 or 롤백하고 트랜잭션 반환.

   (2) logRepo에서 트랜잭션 하나 가져와서, 커밋 or 롤백하고 트랜잭션 반환.

   (3) 두 개의 트랜잭션은 독립적인 것이므로 서로에게 영향을 주지 않음(하나 커밋, 하나 롤백 가능)

   -> 이럴 경우 회원은 저장됐는데 로그는 저장이 되지 않는 데이터 정합성 문제 발생

   -> 따라서, 트랜잭션을 하나로 묶을 필요가 있음

 

3. 트랜잭션을 하나로 묶는 방법 첫번째 : 단일 트랜잭션

   (1) memberService에만 @Transactional을 두고, 나머지 repo에는 빼는 방법

   (2) 이렇게 하면 애초에 트랜잭션이 service에만 생성되므로, 하나의 흐름으로 동작함

   -> 그런데 이렇게 하면 repo 각각에서 트랜잭션이 필요한 요구사항이 있을 경우, 너무 많은 메소드를 따로 만들어야함.

 

4. 트랜잭션을 하나로 묶는 방법 두번째 : 트랜잭션 전파

   (1) memberService와 repo 모두 @Transactional을 사용, 기본 전파 옵션은 REQUIRED

   (2) 이렇게 하면, service에서 새로운 트랜잭션을 하나 가져옴

   (3) 이후 repo에서는 기존에 생성된 트랜잭션을 받아서 처리

 

5. 만약 logRepo에서 롤백되면?

   (1) logRepo가 예외를 던지고, 기존 트랜잭션에 rollbackOnly = true로 바꿈

   (2) memberService는 logRepo에서 예외가 발생해서 올라와서 자기도 예외가 발생했으므로, 롤백

 

6. 새로운 요구사항 : log가 저장이 안되더라도 회원은 저장시키자

   (1) 단순히 memberService가 logRepo에서 올라온 예외를 잡아서 처리하면 될까? 아니다

   (2) logRepo가 예외를 던지면서 트랜잭션에 rollbackOnly = true로 해놓았기 때문이다

   (3) 그래서 memberService가 예외를 잡아서 처리하더라도, 트랜잭션에 rollbackOnly가 true이므로 롤백하고 UnexpectedRollbackException이 발생한다

 

7. 그렇다면 어떻게? REQUIRES_NEW 전파 옵션을 이용해서, logRepo의 트랜잭션을 분리하자

   (1) logRepo의 트랜잭션 전파 옵션을 REQUIRES_NEW로 변경

   (2) logRepo에서는 기존 트랜잭션을 잠시 미뤄두고, 새로운 트랜잭션이 생성됨

   (3) logRepo는 이 트랜잭션에 커밋 or 롤백을 하고 트랜잭션 반환

   (4) memberService는 logRepo의 예외만 처리하면, 정상적으로 커밋 수행

   -> REQUIRES_NEW는 하나의 요청에 두개의 DB 커넥션을 사용하므로, 성능이 중요한 곳에서는 주의해서 사용해야


코테준비

1. 프로그래머스 - 최소 직사각형

- 간단한 수학문제


이펙티브 자바 - 아이템 1 : 생성자 대신 정적 팩터리 메서드를 고려하라

1. 클래스의 인스턴스를 얻는 전통적인 수단 -> public 생성자

 

2. 정적 팩터리 메서드란 public static Boolean valueOf(Boolean b) 와 같은 메서드

 

3. 얻을 수 있는 장점

   (1) 이름을 가질 수 있다

      : new Boolean(true) 보다 Boolean.valueOf(true)가 더 가독성이 뛰어나다

   (2) 호출할 때마다 인스턴스를 새로 생성하지 않아도 된다 

      : Boolean.valueOf(true)는 Boolean 클래스에 이미 만들어진 static 객체를 반환한다. 이렇게 되면 생성비용이 큰 객체가 자주 요청되는 상황에서 좋다. 또한, 인스턴스를 통제할 수 있다(ex. 싱글턴)

   (3) 반환 타입의 하위 타입 객체를 반환할 수 있다

      : 만약 interface에서 static 메서드로 구현체를 반환하면, 사용자는 실제 구현체가 무엇인지 알 필요가 없다. 유연하게 사용할 수 있음

   (4) 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다

      : (3)과 비슷한 뜻이라고 생각

   (5) 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다

      : 무슨말인지 잘 모르겠다...

 

4. 팩토리 메소드에서 흔히 사용되는 명명 방식들(ex. from, of, valueOf, ...)


테니스

이번주 토요일에 서울시협회장배 테니스 대회 양천구 20대 대표로 나가게 돼서, 목동레인보우 클럽에서 같이 연습함

최애 파트너가 다치는 바람에ㅜㅜ 다른 아는 형님이랑 나가게 됐음

오늘 처음 맞춰봤는데, 하... 왜 하필 이 형님이랑 파트너한 경기만 지는거야ㅜㅜ 이 형님이랑 해서 이겨야되는데ㅋㅋ쿠ㅜㅜㅜㅜ

대회 때는 중요한 순간에 더 천천히 침착하게 해서, 상대가 포인트를 어렵게 따도록 해야겠다


TIL을 작성하면서 공부한 것을 한번 더 돌아볼 수 있어서 좋다.

한번 더 머릿속에 넣는 느낌도 들고, 다시 보면서 새롭게 알게되거나 더 자세히 알게되는 것도 있다.

'이펙티브 자바' 책은 아직 많이 어려운 것 같으니, 아이템 1씩 예제를 만들어가면서 해봐야겠다.

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
2023.06.26  (0) 2023.06.26
2023.06.23  (0) 2023.06.23
2023.06.21  (0) 2023.06.21
스프링 DB 2(김영한) 강의 - 섹션 : 스프링 트랜잭션 전파1 

섹션 목적 : 트랜잭션이 둘 이상일 때 어떻게 동작하는지 알아보기

 

1. 기본

   (1) 트랜잭션이 동작하는 순서는 아래와 같다

      ① 트랜잭션 매니저로부터 트랜잭션 하나 획득(실제 DB에는 setAutoCommit(false)로 설정)

      ② 로직 실행

      ③ 트랜잭션 커밋 or 롤백

 

2. 트랜잭션 두 번 사용하기

   (1) 트랜잭션 하나 획득해서 커밋 or 롤백

      ① 커넥션 풀에서 DB와 커넥션 하나 가져와서 프록시로 감싸서 트랜잭션을 반환

      ② 로직 실행

      ③ 커밋하고 커넥션 풀에 커넥션을 반환

   (2) 트랜잭션 하나 획득해서 커밋 or 롤백

      ① 위랑 똑같이 진행. 커넥션 풀에서 가져오는 것이므로 위 트랜잭션과 실제 물리 커넥션을 같을 수 있다. 하지만 애초에 다른 트랜잭션임.

 

3. 트랜잭션 안에 트랜잭션이 있다면?

   (1) 전파 기본 옵션인 REQUIRED를 기준으로 설명

   (2) 외부 트랜잭션 안에 내부 트랜잭션이 있다면, 하나의 트랜잭션으로 진행됨

      이 때, 이렇게 두 개로 나누어진 트랜잭션을 논리 트랜잭션. 실제로 DB에 진행되는 트랜잭션은 물리 트랜잭션이라고 불림.

   (3) 원칙

      ① 모든 논리 트랜잭션 커밋 -> 물리 트랜잭션 커밋

      ② 하나라도 논리 트랜잭션 롤백 -> 물리 트랜잭션 롤백

 

4. Outer 트랜잭션 커밋, Inner 트랜잭션 커밋일 경우

   (1) Outer에서 새로운 실제 물리 트랜잭션 획득

   (2) Inner에서는 Outer에서 만들어진 트랜잭션을 위임받고, 커밋. 하지만, 이 트랜잭션은 위임받은 것이므로, 커밋하더라도 실제 물리 트랜잭션이 커밋되는 것이 아님.

   (3) Outer에서 커밋하면, Outer와 Inner 이 때 커밋됨.

 

5. Outer 롤백, Inner 커밋

   (1) Outer에서 물리 트랜잭션 획득

   (2) Inner에서 커밋하더라도 이는 실제 물리 트랜잭션의 커밋이 아님

   (3) Outer에서 롤백되면, Outer와 Inner 모두 롤백

 

6. Outer 커밋, Inner 롤백

   (1) Outer에서 물리 트랜잭션 획득

   (2) Inner에서 롤백되면, 트랜잭션에 rollback-only를 true로 변경함

   (3) Outer는 커밋할 때, rollback-only를 확인하고 true로 되어있으므로 롤백함.

      ① rollback-only가 true라는 뜻은 내부 트랜잭션 어딘가에서 rollback이 됐음을 의미.

   (4) 개발자는 커밋을 요청했는데 롤백이 됐으므로 이 현상을 알려야함. 그래서 UnexpectedRollbackException을 발생

 

7. Outer와 Inner의 트랜잭션을 분리할 수 있는 전파 옵션 : REQUIRES_NEW

   (1) Outer에서 물리 트랜잭션 획득

   (2) Inner에서도 새로운 물리 트랜잭션 획득

      ① 기존 Outer의 물리 트랜잭션은 잠시 미뤄둠

      ② 이후 Inner가 커밋 or 롤백하면, 미뤄둔 Outer 트랜잭션 시작

   (3) Outer 커밋 or 롤백

   (4) Outer와 Inner는 마치 두 개의 다른 트랜잭션을 사용하듯이 사용할 수 있음

 

8. 그 외에 기타 여러가지 전파 옵션이 존재한다.


   코테 준비

1. 프로그래머스 - 게임 맵 최단거리

- 평범한 BFS 문제. 한번에 맞추지는 못했는데, 조건을 꼼꼼하게 읽자

 

2. 프로그래머스 - 여행경로

- 시간이 남아서 한 문제 더 풀어봄. 그래프를 맵으로 표현하는 것을 떠올릴 수 있는 것이 중요.

 

3. 프로그래머스 - 아이템 줍기

- 한 문제 더. 좌표를 어떻게 표현할 것인가가 중요했던 문제. 힌트는 x2.


내일 '이펙티브 자바' 책이 오니깐, 코테 줄이고 이 책도 읽어보자

'TIL(Today I Learned)' 카테고리의 다른 글

2023.06.28  (0) 2023.06.29
2023.06.27  (0) 2023.06.27
2023.06.26  (0) 2023.06.26
2023.06.23  (0) 2023.06.23
2023.06.22  (0) 2023.06.22

+ Recent posts