Now Loading ...
-
String 클래스
String은 문자열을 나타내는 클래스이다. Java에서 모든 문자열 리터럴(값)은 String 클래스의 인스턴스로 구현된다. (feat. Oracle)
String 객체의 생성과 메모리 저장 방식
String(참고 : 참조형)은 Stack 영역에 실제 값이 저장된 공간의 주소를 저장하고, Heap 영역에 실제 값을 저장한다.
Java에서 String 객체를 생성하는 방법은 2가지이다.
문자열 리터럴(값) : 컴파일 타임에 String Constant Pool에 객체 생성, 이미 존재한다면 해당 객체 재사용
new 키워드 : 일반적인 객체 생성처럼 런타임에 Heap 영역에 객체 생성
ex)
String 멤버함수
// equals() : 문자열 내용 비교 -> boolean
String s1 = "java";
String s2 = "java";
String s3 = new String("java");
System.out.println(s1 == s2); // 출력 : true -> 주소 비교
System.out.println(s1.equals(s2)); // 출력 : true -> 문자열 내용 비교
System.out.println(s1 == s3); // 출력 : false
System.out.println(s1.equals(s3)); // 출력 : true
// length() : 문자열 길이 -> int
String s1 = "";
String s2 = " ";
String s3 = "1234";
String s4 = " 12 34 ";
System.out.println(s1.length()); // 출력 : 0
System.out.println(s2.length()); // 출력 : 1
System.out.println(s3.length()); // 출력 : 4
System.out.println(s4.length()); // 출력 : 7
// charAt() : 특정 인덱스의 문자 -> char
String s1 = "1234";
System.out.println(s1.charAt(2)); // 출력 : 3
// substring() : 특정 인덱스 범위의 문자열 -> String
String s1 = "123456";
System.out.println(s1.substring(1)); // 출력 : 23456 -> begin 부터 마지막 까지
System.out.println(s1.substring(0, 3)); // 출력 : 123 -> begin 부터 end - 1 까지
System.out.println(s1.substring(0, 1)); // 출력 : 1
// contains() : 특정 문자열의 존재 여부 -> boolean
String s1 = "ab cdef";
System.out.println(s1.contains("cd")); // 출력 : true
// indexOf() / lastIndexOf() : 특정 문자열의 시작/마지막 인덱스 -> int
String s1 = "aa bb aa bb";
System.out.println(s1.indexOf("b")); // 출력 : 3
System.out.println(s1.lastIndexOf("b")); // 출력: 10
// startsWith() / endsWith() : 문자열이 특정 접두사 또는 접미사로 시작/끝나는지 확인 > boolean
String s1 = "image_file.png";
System.out.println(s1.startsWith("image")); // 출력 : true
System.out.println(s1.endsWith(".png")); // 출력 : true
// split() : 특정 문자열을 기준으로 문자열을 분리하여 배열로 반환 -> String[]
String s1 = "java python";
String[] languages1 = s1.split(" ");
String[] languages2 = s1.split("");
String[] languages3 = s1.split("a");
System.out.println(Arrays.toString(languages1)); // 출력 : [java,python]
System.out.println(Arrays.toString(languages2)); // 출력 : [j,a,v,a, ,p,y,t,h,o,n]
System.out.println(Arrays.toString(languages3)); // 출력 :[j,v, python] -> python 앞에 공백
// replace() : 문자열 내의 특정 문자열을 다른 문자열로 대체 -> String
String s1 = "aaa bbb aaa";
System.out.println(s1.replace("aa", "bb")); // 출력 : bba bbb bba
System.out.println(s1.replace(" ", "")); // 출력 : aaabbbaaa
System.out.println(s1); // 출력 : aaa bbb aaa
String 참고 사항
String은 불변 객체이므로, 값이 변경되면 새로운 객체가 생성된다. 따라서, 잦은 연산은 성능 저하(메모리 낭비, 속도 저하 등)가 발생할 수 있다. 대안으로는 가변 객체인 StringBuilder와 StringBuffer가 있다.
예시 1 : 객체의 주소
String a = "안녕";
System.out.println(a); // 출력 : 안녕
System.out.println(a.hashCode()); // 출력 : 1611021
a += "하세요";
System.out.println(a); // 출력 : 안녕하세요
System.out.println(a.hashCode()); // 출력 : 803356551
예시 2 : 연산 속도 비교 : String vs StringBuilder
long startTime_pre = System.nanoTime();
// 1. String 연산
String s = "";
for (int i = 0; i < 100_000; i++) {
// for문의 코드 실행할 때마다 새로운 객체 생성
s += i; // s = s + String.valueOf(i);
}
long endTime_pre = System.nanoTime();
System.out.println("String 실행 시간: " + (endTime_pre - startTime_pre) + " ns");
// ---------------> String 실행 시간: 3524436890 ns ( = 약 3.524초)
long startTime_ref = System.nanoTime();
// 2. StringBuilder 연산
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
// 같은 객체를 수정
sb.append(i); // sb.append(String.valueOf(i));
}
String s_sb = sb.toString();
long endTime_ref = System.nanoTime();
System.out.println("StringBuilder 실행 시간: " + (endTime_ref - startTime_ref) + " ns");
// ---------------> StringBuilder 실행 시간: 3767563 ns ( = 약 0.0038초)
// 실행시간 차이가 크다.
// 즉, 잦은 연산이 필요할 때 CPU와 메모리를 낭비하고 싶지 않다면
// StringBuilder나 StringBuffer를 사용해야 할 것이다.
Java 컴파일러는 자동으로 (String s = a + b + c)와 같은 표현식을 StringBuilder를 사용하여 최적화한다.
String s = "Hello" + " " + "World!";
// 위 코드는 컴파일러가 아래의 코드로 자동 변환하여 최적화한다.
String s = new StringBuilder()
.append("Hello")
.append(" ")
.append("World!")
.toString();
참고 자료
https://docs.oracle.com/javase/8/docs/api/
https://stackoverflow.com/questions/16783971/string-constant-pool-vs-string-pool
https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.5
https://www.youtube.com/watch?v=gp6NY01XFoE
https://stackoverflow.com/questions/47605/string-concatenation-concat-vs-operator
https://stackoverflow.com/questions/5234147/why-stringbuilder-when-there-is-string
-
기본 래퍼 클래스(Primitive Wrapper Class)
기본 래퍼 클래스(Primitive Wrapper Class)는 8개의 기본형을 캡슐화(객체로 변환)하여, 이를 다른 클래스나 메서드에서 객체로 사용할 수 있도록 해주는 클래스이다 (feat. Wikipedia)
기본 래퍼 클래스의 메모리 저장 방식
기본 래퍼 클래스(참고 : 참조형)는 Stack 영역에 실제 값이 저장된 공간의 주소를 저장하고, Heap 영역에 실제 값을 저장한다.
ex)
기본 래퍼 클래스 종류
기본형
기본 래퍼 클래스
byte
Byte
char
Character
int
Integer
float
Float
double
Double
boolean
Boolean
long
Long
short
Short
오토 박싱(Auto Boxing), 오토 언박싱(Auto Unboxing)
박싱 : 기본형을 기본 래퍼 클래스의 객체(인스턴스)로 변환하는 것
언박싱 : 기본 래퍼 클래스의 객체(인스턴스)에 저장된 실제 값을 기본형으로 변환하는 것
오토 박싱과 오토 언박싱은 자바 컴파일러가 상황에 따라 박싱과 언박싱을 자동으로 처리해주는 것을 의미한다. (JDK 1.5 버전부터 도입)
Integer i_ref = 1000; // 오토 박싱
// Integer i_ref = Integer.valueOf(1000); // 박싱
int i_pre = i_ref; // 오토 언박싱
// int i_pre = i_ref.intValue(); // 언박싱
int sum_prem = i_pre + i_ref; // 오토 언박싱
// int sum_prem = i_pre + i_ref.intValue(); // 언박싱
Integer sum_ref = i_pre + i_ref; // 오토 언박싱과 오토 박싱
// Integer sum_ref = Integer.valueOf(i_pre + i_ref.intValue()); // 언박싱과 박싱
기본 래퍼 클래스(ex) Integer) 참고 사항
Java 9 버전부터는 기본 래퍼 클래스의 생성자 사용이 deprecated 처리되었다.
Integer i1 = 777;
Integer i2 = Integer.valueOf(777);
Integer i3 = Integer.valueOf("777"); // 박싱
Integer i4 = "777"; // 컴파일 오류
Integer i5 = new Integer(777); // 컴파일 오류
Integer i6 = new Integer("777"); // 컴파일 오류
자바 컴파일러는 오토 박싱 시 생성자 대신 Integer.valueOf()를 사용하며, 해당 메서드는 캐싱된 Integer 객체를 반환한다.
JVM은 -128에서 127까지의 int 값에 대한 Integer 객체를 캐싱하고, 이러한 객체들은 Heap 영역의 내의 Constant Pool에 생성된다. 메모리 절약 및 속도 향상의 이점이 있다.
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // 출력 : true -> i1과 i2는 같은 객체를 참조 -> 주소 비교
System.out.println(i1.equals(i2)); // 출력 : true -> Integer의 값 비교
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // 출력 : false -> i3와 i4는 다른 객체를 참조 -> 주소 비교
System.out.println(i3.equals(i4)); // 출력 : true -> Integer의 값 비교
// Integer 클래스의 valueOf() 메서드
public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc {
...
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
}
기본 생성 클래스는 불변 객체이므로, 값이 변경되면 새로운 객체가 생성된다. 따라서, 잦은 연산은 성능 저하(메모리 낭비, 속도 저하 등)가 발생할 수 있다.
예시 1 : 객체의 주소
Integer i = 1000;
System.out.println(System.identityHashCode(i)); // 출력 : 1915318863
i += 500;
System.out.println(System.identityHashCode(i)); // 출력 : 1283928880
예시 2 : 연산 속도 비교 : 기본형 vs 기본 래퍼 클래스
long startTime_pre = System.nanoTime();
// 1. 기본형 연산
int sum_pre = 0;
for (int i = 0; i < 10_000; i++) {
sum_pre += i;
}
long endTime_pre = System.nanoTime();
System.out.println("기본형 실행 시간: " + (endTime_pre - startTime_pre) + " ns");
// ---------------> 기본형 실행 시간: 101979 ns
long startTime_ref = System.nanoTime();
// 2. 기본 래퍼 클래스 연산
Integer sum_ref = 0; // Integer.valueOf(0);
for (int i = 0; i < 10_000; i++) {
// for문의 코드 실행할 때마다 새로운 객체 생성
sum_ref += i; // sum_ref = Integer.valueOf(sum_ref.intValue() + i);
}
long endTime_ref = System.nanoTime();
System.out.println("기본 래퍼 클래스 실행 시간: " + (endTime_ref - startTime_ref) + " ns");
// ---------------> 기본 래퍼 클래스 실행 시간: 1822995 ns
// 실행시간 차이가 크다.
// 즉, 잦은 연산이 필요할 때 CPU와 메모리를 낭비하고 싶지 않다면
// 기본 타입인 int을 사용하거나
// 객체를 재사용할 수 있는 새로운 클래스를 정의해야 할 것이다.
참고 자료
https://stackoverflow.com/questions/13098143/why-does-the-behavior-of-the-integer-constant-pool-change-at-127
https://stackoverflow.com/questions/3689745/what-exactly-does-comparing-integers-with-do
https://stackoverflow.com/questions/3131136/integers-caching-in-java
https://inpa.tistory.com/entry/JAVA-%E2%98%95-wrapper-class-Boxing-UnBoxing
https://jaehoney.tistory.com/101
-
참조형(Reference Data Type)
데이터 타입 또는 자료형은 컴퓨터 과학과 프로그래밍 언어에서 실수치, 정수, 불린 자료형 따위의 여러 종류의 데이터를 식별하는 분류이다. (feat. 위키백과)
참조형의 메모리 저장 방식
참조형은 Stack 영역에 실제 값이 저장된 공간의 주소를 저장하고, Heap 영역에 실제 값을 저장한다.
ex)
참조형 종류 (기본형 제외 나머지)
대표적인 참조형을 살펴보겠다.
타입
설명
배열
동일한 타입의 데이터를 고정된 크기의 연속된 메모리 공간에 저장하는 자료구조
ArrayList (Collection)
크기가 동적으로 변하는 배열 기반의 자료구조
String
문자열을 다루는 클래스
Wrapper 클래스
기본형을 객체로 감싸서 다룰 수 있게 하는 클래스 (ex) Integer, Long)
사용자 정의 클래스
개발자가 정의한 데이터 구조를 표현하는 클래스
참조형 선언 및 초기화, 출력, 비교
배열
// 선언 및 초기화
int[] arr = {1, 2};
// 또는
// int[] arr = new int[]{1, 2};
// 또는
// int[] arr = new int[2]; // arr[0], arr[1]의 초기값은 0
// arr[0] = 1;
// arr[1] = 2;
int[] arr_same = {1, 2};
// 출력
System.out.println(arr); // 출력 : [I@7229724f -> 주소
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " "); // 출력 : 1 2
}
System.out.println(Arrays.toString(arr)); // 출력 : [1, 2]
// 비교
System.out.println(arr == arr_same); // 출력 : false -> 주소 비교
System.out.println(Arrays.equals(arr, arr_same)); // 출력 : true -> 배열의 내용 비교
ArrayList
// 선언 및 초기화
List<Integer> list = new ArrayList<>(Arrays.asList(1,2));
// 또는
// List<Integer> list = new ArrayList<>();
// list.add(1);
// list.add(2);
List<Integer> list_same = new ArrayList<>(Arrays.asList(1,2));
// 출력
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " "); // 출력 : 1 2
}
System.out.println(list); // 출력 : [1, 2] -> ArrayList 클래스에서 toString() 메서드가 Override돼있음
// 비교
System.out.println(list == list_same); // 출력 : false -> 주소 비교
System.out.println(list.equals(list_same)); // 출력 : true -> List의 내용 비교
String
// 선언 및 초기화
String str_ltr_1 = "hello"; // 문자열 리터럴을 사용하여 생성
String str_ltr_2 = "hello";
String str_new_1 = new String("hello"); // new 키워드를 사용하여 생성
String str_new_2 = new String("hello");
// 출력
System.out.println(str_ltr_1); // 출력 : hello
System.out.println(str_new_1); // 출력 : hello
// 비교
System.out.println(str_ltr_1 == str_ltr_2); // 출력 : true -> 리터럴로 생성한
// 문자열은 같은 객체(String pool) -> 주소 비교
System.out.println(str_ltr_1 == str_new_1); // 출력 : false -> 다른 객체
System.out.println(str_new_1 == str_new_2); // 출력 : false -> new로 생성한
// 문자열은 다른 객체
System.out.println(str_ltr_1.equals(str_ltr_2)); // 출력 : true -> 문자열의 내용 비교
System.out.println(str_ltr_1.equals(str_new_1)); // 출력 : true
System.out.println(str_new_1.equals(str_new_2)); // 출력 : true
사용자 정의 클래스
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
// 선언 및 초기화
Person p1 = new Person("백엔드", 25);
Person p2 = new Person("백엔드", 25);
// 출력
System.out.println(p1); // 출력 : Person@4c873330 -> 주소
// 비교
System.out.println(p1 == p2); // 출력 : false -> 주소 비교
System.out.println(p1.equals(p2)); // 출력 : true -> 객체의 내용 비교
// Person 클래스에서 equals() 메서드를
// 개발자가 직접 Override하여 작성했음
}
}
-
-
트랜잭션
트랜잭션과 세션
트랜잭션은 더 이상 쪼갤 수 없는 업무 처리의 최소 단위를 의미한다.
트랜잭션은 ACID를 보장해야 한다. ACID란 트랜잭션이 안전하게 수행되기 위한 4가지 필수적인 성질을 말한다.
Atomicity(원자성) : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
Consistenecy(일관성) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어, 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
Isolation(격리성) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야 한다.
격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation Level)을 선택할 수 있다.
Durability(지속성) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션의 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋(Commit)이라 하고, 작업 중 하나라도 실패해서 트랜잭션 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.
트랜잭션 격리 수준(Isolation Level)
격리성(Isolation)을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데, 이렇게 하면 동시 처리 성능이 매우 나빠진다. 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.
READ UNCOMMITED(커밋되지 않은 읽기)
READ COMMITTED(커밋된 읽기)
REPEATABLE READ(반복 가능한 읽기)
SERIALIZABLE(직렬화 가능)
-> 일반적으로 READ COMMITTED(커밋된 읽기) 트랜잭션 격리 수준을 많이 사용한다.
데이터베이스 연결 구조와 세션
사용자는 애플리케이션 서버, DB 접근 툴과 같은 클라이언트를 사용해서 데이터베이스 서버에 접근할 수 있다.
클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺는데, 이때 데이터베이스 서버는 내부에 세션이라는 것을 만든다. 그리고 앞으로 해당 커넥션을 통한 모든 요청은 이 세션을 통해서 실행하게 된다.
개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.
사용자가 커넥션을 닫거나, DB 관리자가 세션을 강제로 종료하면 세션은 종료된다.
트랜잭션 모드 - 자동 커밋과 수동 커밋
DBMS가 트랜잭션을 처리하는 모드는 자동 커밋과 수동 커밋이 있다.
트랜잭션 모드는 연결 단위(세션)별로 설정하며, 연결 중에도 언제든지 변경할 수 있다.
자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 commit을 호출한다.
set autocommit true; // 자동 커밋 모드 설정
insert into member(member_id, money) values ('data1',10000);
// 자동 커밋
insert into member(member_id, money) values ('data2',10000);
// 자동 커밋
자동 커밋에서는 쿼리를 하나하나 실행할 때마다 자동으로 커밋이 되어버리기 때문에 우리가 원하는 트랜잭션 기능을 제대로 사용할 수 없다. 따라서, 트랜잭션 기능을 제대로 수행하려면 수동 커밋을 사용해야 한다.
수동 커밋으로 설정하면 이후에 꼭 commit이나 rollback을 호출해야 한다.
set autocommit false; // 수동 커밋 모드 설정
insert into member(member_id, money) values ('data3',10000);
insert into member(member_id, money) values ('data4',10000);
commit; // 수동 커밋
보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.
트랜잭션 간단한 예시와 문제
이전 글 참고
MemberRepositoryV1 클래스
@Repository
public class MemberRepositoryV1 implements MemberRepository {
// 기존 메소드
// ...
// findById(String memberId)...
// update(String memberId, int money)...
// ...
// 커넥션 유지가 필요한 findById, update 메소드 추가
// 기존 findById, update 메소드와 같은 내용이지만, 2가지 부분이 다르다.
// 1. con = getConnection() 삭제
// 2. dbcUtils.closeConnection(con) 삭제
public Member findById(Connection con, String memberId) throws SQLException {
...
}
public void update(Connection con, String memberId, int money) throws SQLException {
...
}
}
MemberServiceV0 클래스
@Service
public class MemberServiceV1 {
private static final Logger log = LoggerFactory.getLogger(MemberRepositoryJdbc.class);
private final DataSource dataSource;
private final MemberRepository memberRepository;
public MemberServiceV1(DataSource dataSource, MemberRepository memberRepository) {
this.dataSource = dataSource;
this.memberRepository = memberRepository;
}
// 간단한 계좌이체
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); //트랜잭션 시작
bizLogic(con, fromId, toId, money); //비즈니스 로직
con.commit(); //성공시 커밋
} catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
} finally {
if (con != null) {
try {
con.setAutoCommit(true); //커넥션 풀 고려
con.close(); //커넥션 풀을 사용하므로 커넥션이 종료되는 것이 아니라 풀에 반납됨
} catch (Exception e) {
log.info("error", e);
}
}
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
}
문제
서비스 계층은 트랜잭션을 사용하기 위해서 JDBC 기술에 의존하고 있다. 향후 JPA 같은 다른 기술로 바꾸어 사용하게 되면 서비스 코드도 모두 함께 변경해야 한다. (데이터 접근 기술마다 트랜잭션을 사용하는 코드가 다름)
Repository에서 Connection을 인자로 받는 새로운 findById, update 메소드를 작성한 것처럼, 똑같은 기능도 트랜잭션용 기능과 트랜잭션을 유지하지 않아도 되는 기능으로 분리해야 한다.
트랜잭션 적용 코드(try, catch, finally 부분)에 반복이 많다.
스프링은 위의 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다.
-> 스프링이 제공하는 트랜잭션 기술
참고 자료
“김영한 스프링 DB 1편”
http://wiki1.kr/index.php/트랜잭션
http://soen.kr/book/sql/book/19-2.htm
-
커넥션 풀과 DataSource
데이터베이스 커넥션 생성
이전 글에서는, 커넥션(Connection 객체)을 획득하기 위해 DriverManager를 사용했다.
DriverManager는 JDBC 드라이버를 통해 커넥션을 “획득”하는데, 아래의 과정은 JDBC 드라이버가 커넥션을 “생성”하는 과정에 대한 설명이다.
커넥션 생성 과정
애플리케이션 로직은 JDBC 드라이버를 통해 커넥션을 조회한다.
JDBC 드라이버는 데이터베이스 서버와 TCP/IP 커넥션을 연결한다. 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생한다.
TCP/IP 커넥션이 연결되면, JDBC 드라이버는 ID, PW, 기타 부가정보를 데이터베이스 서버에 전달한다.
데이터베이스 서버는 ID, PW를 통해 내부 인증을 완료하고, 내부에 세션을 생성한다.
데이터베이스 서버는 커넥션 생성이 완료되었다는 응답을 보낸다.
JDBC 드라이버는 커넥션(Connection) 객체를 생성해서 클라이언트(애플리케이션 서버)에 반환한다.
매번 커넥션을 새로 생성할 때 발생하는 문제
위와 같이, 커넥션을 새로 생성하는 과정은 복잡하기 때문에 애플리케이션 로직에서 매번 커넥션을 새로 생성한다면 다음과 같은 문제들이 발생한다.
데이터베이스 서버는 물론이고 애플리케이션 서버에서도 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다.
고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐만 아니라 커넥션을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다.
이런 문제들을 한번에 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀(커넥션을 관리하는 풀) 방법이다.
커넥션 풀
커넥션 풀은 미리 일정 수의 데이터베이스 커넥션을 생성하여 풀로 관리하고, 필요할 때마다 풀에서 기존의 연결을 재사용하도록 하는 기법이다.
이전처럼 DriverManager를 통해 새로운 커넥션을 획득하는 방식이 아니라 이미 생성되어 있는 커넥션을 획득하는 방식이다.
따라서, 커넥션 풀 방식을 사용하면 애플리케이션 로직을 수행할 때 커넥션을 새로 생성하는 복잡한 과정을 수행하지 않는다.
커넥션 풀은 개념적으로 단순해서 직접 구현할 수도 있지만, 사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용하는 것이 좋다.
대표적인 커넥션 풀 오픈소스는 commons-dbcp2 , tomcat-jdbc pool , HikariCP 등이 있으며, Spring Boot는 기본적으로 HikariCP를 커넥션 풀로 사용한다.
커넥션 풀은 별도의 쓰레드를 사용해서 커넥션을 채운다.
커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리는 일이기 때문에, 애플리케이션을 실행할 때 커넥션 풀을 채울 때 까지 마냥 대기하고 있다면 애플리케이션 실행 시간이 늦어진다. 따라서 별도의 쓰레드를 사용해서 커넥션 풀을 채워야 애플리케이션 실행 시간에 영향을 주지 않는다.
커넥션 풀 초기화와 연결 상태
애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
기본값은 보통 10개이며, 적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테스트를 통해서 정해야 한다.
커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다. 따라서 DB에 무한정 연결이 생성되는 것을 막아주어서 DB를 보호하는 효과도 있다.
커넥션 풀에 들어 있는 커넥션은 TCP/IP로 데이터베이스 서버와 커넥션이 연결되어 있는 상태이기 때문에, 언제든지 즉시 SQL을 데이터베이스 서버에 전달할 수 있다.
커넥션 풀 사용 과정
애플리케이션 로직은 커넥션 풀을 통해 커넥션을 조회한다.
커넥션 풀에 커넥션을 요청하면, 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스 서버에 전달하고 그 결과를 받아서 처리한다.
커넥션을 모두 사용하고 나면, 커넥션을 종료하는 것이 아니라 다음에 다시 사용할 수 있도록 해당 커넥션이 살아있는 상태로 커넥션 풀에 반환한다.
DataSource
커넥션을 획득하는 방법
지금까지 설명한 것처럼, 애플리케이션 로직에서 커넥션을 획득하는 방법은 대표적으로 2가지 방법이 있다. (실무에서는 기본으로 커넥션 풀 사용)
직접 DriverManager 사용 - 새로운 커넥션
커넥션 풀 사용 - 이미 생성되어 있는 커넥션
그러나, 애플리케이션 로직에서 DriverManager를 사용해서 커넥션을 획득하다가 HikariCP 같은 커넥션 풀을 사용하도록 변경할 때 문제가 발생한다.
의존관계가 DriverManager에서 HikariCP로 변경되기 때문에, 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다.
이런 문제를 해결하기 위해 자바에서는 커넥션을 획득하는 방법을 추상화하는 인터페이스인 DataSoucre를 제공한다.
DataSouce
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
...
}
애플리케이션 로직은 커넥션을 획득할 때 DataSource 인터페이스에만 의존하면 된다.
대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었으며, 커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 갈아끼우기만 하면 된다.
DriverManager는 DataSource 인터페이스를 사용하지 않기 때문에, DriverManager를 사용하다가 DataSource 기반의 커넥션 풀을 사용하도록 변경하면 관련 코드를 다 고쳐야 한다.
이런 문제를 해결하기 위해 스프링은 DriverManager도 DataSource를 통해서 사용할 수 있도록 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다.
DataSource 사용 예시 - Spring
이전 글 참고
프로퍼티 파일 작성 (src/main/resources/application.properties)
jdbc.url=jdbc:mysql://(DB 서버 주소):3306/(DB명)?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
jdbc.username=(DB 사용자 이름)
jdbc.password=(DB 사용자 비밀번호)
스프링 설정 클래스 작성 (src/main/java/com/example/myproject/config/)
DispatcherConfig
@Configuration
@ComponentScan(basePackages = "com.example.myproject")
@EnableWebMvc
public class DispatcherConfig implements WebMvcConfigurer {
...
}
DataSourceConfig
@Configuration
@PropertySource("classpath:application.properties")
public class DataSourceConfig {
// Environment를 사용하여 프로퍼티 파일의 값을 로드
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
// 1. DriverManagerDataSource
// DriverManagerDataSource dataSource =
// new DriverManagerDataSource(jdbc.url, jdbc.username, jdbc.password);
// 2. 커넥션 풀의 DataSource
HikariDataSource dataSource = new HikariDataSource();
// 필수
dataSource.setJdbcUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
// 선택
dataSource.setMaximumPoolSize(20); // 설정하지 않으면 기본값은 10
return dataSource;
}
}
MemberRepositoryJdbcDataSource 클래스 작성
@Repository
public class MemberRepositoryJdbcDataSource implements MemberRepository {
// 외부에서 DataSource를 주입 받아서 사용
private final DataSource dataSource;
public MemberRepositoryJdbcDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
return con;
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
//save(Member member)...
//findById(String memberId)...
//update(String memberId, int money)....
//delete(String memberId)....
}
참고 자료
“김영한 스프링 DB 1편”
https://docs.oracle.com/javase/8/docs/api/javax/sql/DataSource.html
-
JDBC
애플리케이션 서버와 데이터베이스
커넥션 연결 : 주로 TCP/IP를 사용해서 커넥션을 연결한다.
SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.
문제
각각의 데이터베이스마다 커넥션 연결, SQL 전달, 결과를 응답 받는 방법이 모두 다르다. 즉, 데이터베이스 종류를 변경하면 애플리케이션 서버의 데이터베이스 사용 코드도 함께 변경해야 한다.
이런 문제를 해결하기 위해 JDBC라는 자바 표준이 등장한다.
JDBC
JDBC(Java Database Connectivity)는 데이터베이스와의 연결 및 상호작용을 위한 자바 API이다.
JDBC API는 자바 패키지 java.sql과 javax.sql에 여러 표준 인터페이스와 클래스를 정의하고 있다.
java.sql 패키지는 JDBC API의 핵심 부분을 구성하며, javax.sql 패키지는 JDBC API의 확장 부분을 구성한다.
각 DB 벤더는 자신의 DB에 맞도록 JDBC API에서 정의한 표준 인터페이스들을 구현해서 라이브러리로 제공하는데, 이것을 JDBC 드라이버라고 한다.
즉, JDBC 표준 인터페이스의 구현체는 JDBC 드라이버이다.
개발자는 JDBC 표준 인터페이스만 사용해서 개발하면 되고, 애플리케이션 로직은 JDBC 표준 인터페이스에만 의존한다.
데이터베이스를 다른 종류의 데이터베이스로 변경하고 싶다면 JDBC 드라이버만 변경하면 되므로, 애플리케이션 서버의 사용 코드를 그대로 유지할 수 있다.
주요 JDBC 표준 인터페이스와 클래스
Connection, Statement, ResultSet (인터페이스, java.sql)
데이터베이스와의 연결, 쿼리 실행, 쿼리 결과의 표준을 정의한다.
Driver (인터페이스, java.sql)
데이터베이스와 연결을 설정하는 방법의 표준을 정의한다.
DriverManager (클래스, java.sql)
JDBC 드라이버들을 관리하고, 등록된 드라이버 중에서 적합한 드라이버의 Connection 객체를 획득하는 기능을 제공한다.
DataSource (인터페이스, javax.sql)
Connection 객체를 획득하는 방법의 표준을 정의한다.
커넥션 풀과 DataSource
SQLException (클래스, java.sql)
JDBC에서 데이터베이스와의 상호작용 중에 발생할 수 있는 모든 예외를 처리하는 기본 예외 클래스이다.
Connection, Statement, ResultSet
Connection 주요 메소드
public interface Connection extends Wrapper, AutoCloseable {
Statement createStatement() throws SQLException; // SQL 쿼리를 실행할 수 있는 Statement 객체를 생성
PreparedStatement prepareStatement(String var1) throws SQLException; // 미리 컴파일된 SQL 쿼리를 실행할 수 있는 PreparedStatement 객체를 생성
void setAutoCommit(boolean var1) throws SQLException; // 자동 커밋 모드로 설정
void commit() throws SQLException; // 현재 트랜잭션을 커밋
void rollback() throws SQLException; // 현재 트랜잭션을 롤백
void close() throws SQLException; // Connection 종료
}
Statement 주요 메소드
public interface Statement extends Wrapper, AutoCloseable {
ResultSet executeQuery(String var1) throws SQLException; // SELECT 문을 실행하고, 결과를 ResultSet 객체로 반환
int executeUpdate(String var1) throws SQLException; // INSERT, UPDATE, DELETE 문을 실행하고, 영향을 받은 행의 수를 반환
boolean execute(String var1) throws SQLException; // SQL 문을 실행하고, 쿼리의 실행 결과를 나타내는 boolean 값을 반환
void close() throws SQLException; // Statement 종료
}
ResultSet 주요 메소드
// ResultSet은 테이블 형식이다.
public interface ResultSet extends Wrapper, AutoCloseable {
boolean next() throws SQLException; // 커서를 다음 행으로 이동시키며, 결과 집합에 더 이상 행이 없으면 false를 반환
String getString(String var1) throws SQLException; // 지정된 열의 문자열 값을 반환
int getInt(String var1) throws SQLException; // 지정된 열의 정수 값을 반환
double getDouble(String var1) throws SQLException; // 지정된 열의 실수 값을 반환
void close() throws SQLException; // ResultSet 종료
}
Driver, DriverManager
public interface Driver {
// 주어진 URL과 정보를 바탕으로 데이터베이스와 연결하고, 그 연결 객체(Connection 객체)를 반환
Connection connect(String var1, Properties var2) throws SQLException;
...
}
public class DriverManager {
...
// Driver 객체를 DriverManager에 등록
public static void registerDriver(Driver driver) throws SQLException {
registerDriver(driver, (DriverAction)null);
}
public static void registerDriver(Driver driver, DriverAction da) throws SQLException {
...
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
}
// Driver 객체의 connect 메소드를 호출하여 Connection 객체를 획득
public static Connection getConnection(String url, String user, String password) throws SQLException {
...
return getConnection(url, info, Reflection.getCallerClass());
}
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
...
Iterator var5 = registeredDrivers.iterator();
while(true) {
while(var5.hasNext()) {
DriverInfo aDriver = (DriverInfo)var5.next();
...
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return con;
}
...
}
...
}
Driver 클래스 로딩 시 Driver 객체가 DriverManager에 등록되는데, Driver 클래스를 로딩하는 방식은 아래와 같이 2가지 방식이 있다.
명시적 로딩 : Class.forName(“…”) 메소드를 사용하여 Driver 클래스를 명시적으로 로드하는 방식
자동 로딩 : Java 6 이후, JDBC 4.0부터는 JDBC 드라이버 JAR 파일의 “META-INF/services/java.sql.Driver” 파일이 포함되어 있으면 자동으로 Driver 클래스가 로드되는 방식
DriverManager는 JDBC 드라이버들의 Driver 객체를 관리하고, 등록된 Driver 객체들의 connect 메소드를 호출하여 Connection 객체가 얻어지면 해당 Connection 객체를 반환하는 기능을 제공한다.
Connection 객체를 얻기 위한 이런 복잡한 작업들을 DriverManager가 해주기 때문에, 개발자는 간편하게 DriverManager만 사용해서 Connection 객체를 얻으면 된다.
DriverManager와 JDBC 드라이버 - MySQL 예시
MySQL JDBC 드라이버 JAR 파일의 “com.mysql.cj.jdbc 패키지” 내에는 Driver, Connection, Statement, ResultSet 등 JDBC 표준 인터페이스를 구현하는 클래스들이 존재한다.
MySQL JDBC 드라이버의 Driver 클래스가 로드될 때, Driver 클래스의 정적 초기화 블록이 실행되어 Driver 객체가 DriverManager에 등록된다.
public class Driver extends NonRegisteringDriver implements java.sql.Driver
{
public Driver() throws SQLException { }
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
애플리케이션에서 DriverManager.getConnection(url, user, password) 메소드를 호출하면, 해당 메소드에서는 등록된 Driver 객체들의 connect 메소드를 호출하여 Connection 객체가 얻어지면 해당 Connection 객체를 반환한다.
MySQL JDBC 드라이버의 Driver 객체의 connect 메소드는 MySQL 서버에 연결한 후 ConnectionImpl 객체(Connection 인터페이스의 구현체)를 반환한다.
JDBC를 사용한 간단한 CRUD 예시
DBConnectUtil 클래스 작성 (Connection 획득)
public class DBConnectionUtil {
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
Member 클래스 작성
public class Member {
private String memberId;
private int money;
public Member() {
}
public Member(String memberId, int money) {
this.memberId = memberId;
this.money = money;
}
...
// Getter, Setter 메소드
// toString, equals, hashCode 메소드 - @Override
}
JdbcMemberRepository 클래스 작성
@Repository
public class MemberRepositoryJdbc implements MemberRepository {
private static final Logger log = LoggerFactory.getLogger(MemberRepositoryJdbc.class);
// Coonection 획득
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
// 리소스 정리
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
@Override
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
@Override
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
// 조회 결과가 항상 1건(member_id는 PK)이므로
// while 대신에 if 사용
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
@Override
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
@Override
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
}
참고 자료
“김영한 스프링 DB 1편”
https://ko.wikipedia.org/wiki/JDBC
https://www.ibm.com/docs/ko/i/7.3?topic=connections-java-drivermanager-class
https://www.ibm.com/docs/ko/i/7.3?topic=exceptions-java-sqlexception-class
Touch background to close