
자바는 싱글 프로세스, 멀티 스레드 환경이다.
이 말은 JVM 한 개가 프로세스 하나로 실행되고, 그 안에 여러 스레드가 돌아간다는 의미다.
실제로 JVM이 실행되면 아래와 같은 스레드들이 기본적으로 동작한다.
main thread : 우리가 작성한 main 메서드가 실행되는 스레드다.GC thread : 메모리 누수를 막기 위해 돌아다니며 가비지를 수거하는 녀석.JIT Compiler thread : 반복적으로 실행되는 코드를 네이티브 코드로 바꿔 성능을 최적화한다.이처럼 자바는 기본적으로 멀티 스레드 환경이며, 우리가 신경 쓰지 않아도 많은 스레드가 백그라운드에서 열일하고 있다.
그런데 웹 서버를 띄우고 HTTP 요청을 받기 시작하면 이야기가 복잡해진다.
여기서 등장하는 것이 바로 스레드 풀(Thread Pool) 이다.
Spring Boot를 실행하면 내장 Tomcat 서버가 같이 켜지는데, 이 Tomcat은 자체적으로 스레드 풀을 만든다.
그리고 각 HTTP 요청이 들어올 때마다 워커 스레드(worker thread) 를 하나 꺼내서 해당 요청을 처리한다.
🔁 하나의 요청 → 하나의 워커 스레드
처리가 끝나면 다시 풀(pool)로 반납하는 방식이다.
워커 스레드가 클라이언트의 요청을 받아 Controller → Service → Repository 순으로 코드를 실행한다.
그 과정에서 DB에 접근해야 하면?
HTTP 요청 → 워커 스레드 → 커넥션 풀 → DB 통신 → 결과 반환
스레드는 많은데 커넥션이 적으면?
TimeoutException 이 발생한다.커넥션이 너무 많으면?
Too many connections 에러가 날 수 있다.결국 적절한 커넥션 풀 설정이 중요하다.
(보통 CPU 코어 수 × 2 + 1 공식을 참고한다.)
이제 스프링스럽게 개발하기 위한 핵심 키워드,
IoC (제어의 역전) 와 DI (의존성 주입) 이야기를 빼놓을 수 없다.
Spring Boot가 실행되면 내부적으로 IoC 컨테이너(ApplicationContext)가 만들어진다.
이 컨테이너는 @Component, @Service, @Repository 등 어노테이션이 붙은 클래스를 Bean으로 등록한다.
이렇게 등록된 Bean들은 스프링이 관리한다.
우리가 직접 new 하지 않아도 스프링이 객체를 생성하고 의존성까지 주입해준다.
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
}
위 코드는 스프링의 DI(Dependency Injection)를 활용한 대표적인 예다.
우리는 new PostService() 라고 한 적이 없는데도, 스프링이 알아서 주입해준다.
이게 바로 IoC + DI의 마법이다. ✨
예외는 어차피 터진다.
중요한 건 터졌을 때 어떻게 대응하느냐 이다.
예외가 처리되지 않고 main까지 올라오면 자바 프로세스 자체가 종료된다.
Spring Boot에선 다행히 서버가 죽지는 않지만,
클라이언트는 이유도 모른 채 500 Internal Server Error만 받아보게 된다.
스프링에서는 전역 예외 처리기를 만들어 깔끔하게 잡아주는 것이 좋다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArg(IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("BAD_REQUEST", e.getMessage()));
}
}
@RestControllerAdvice 를 사용하면 특정 예외에 대해 공통된 응답을 만들 수 있다.
| 유형 | 설명 | 예시 |
|---|---|---|
| 단순 컴파일 에러 | 문법 에러 | 오타, 존재하지 않는 변수 |
| CheckedException | 예외 처리가 강제됨 | IOException, SQLException |
| UncheckedException | 런타임 중 발생 | NullPointerException, IllegalArgumentException |
💡 현실적으로 우리가 가장 많이 다루는 건 UncheckedException 이다.
1차 세미나까지는 데이터를 메모리에 저장했다.
서버를 재시작하면 데이터가 다 날아갔다.
데이터를 영구적으로 저장하고, 여러 사용자 요청에 대응하려면 DB가 필요하다.
그래서 우리는 데이터베이스라는 시스템을 사용한다.
이 시스템은 데이터를 저장, 조회, 수정, 삭제하기 위한 전문적인 환경을 제공한다.
관계형 데이터베이스(RDB)는 말 그대로 "관계"가 중심이다.
모든 테이블에는 유일한 값(PK)이 필요하다.
id 같은 값은 PK로 사용된다. 중복되거나 NULL이면 안 된다.
다른 테이블의 PK를 참조하는 값이다.
예를 들어 post 테이블이 user_id 컬럼을 가지고 있다면,
이는 user 테이블의 id를 참조하는 외래 키(FK)다.
| 관계 | 설명 |
|---|---|
| 1:1 | 유저 1명 ↔ 프로필 1개 |
| 1:N | 유저 1명 ↔ 게시글 여러 개 |
| N:M | 유저 여러 명 ↔ 강의 여러 개 (중간 테이블 필요) |
-- 테이블 생성
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);
-- 데이터 삽입
INSERT INTO user VALUES (1, '민장규');
-- 데이터 조회
SELECT * FROM user;
-- 데이터 수정
UPDATE user SET name = '장규' WHERE id = 1;
-- 데이터 삭제
DELETE FROM user WHERE id = 1;
자바로 직접 SQL을 날릴 수 있다.
하지만 JDBC를 사용하면 다음과 같은 문제가 있다.
그래서 나온 것이 바로…
JPA는 자바 ORM의 표준 인터페이스이고,
Hibernate는 이를 구현한 실질적인 라이브러리이다.
우리는 엔티티 클래스를 정의하고,
@Entity, @Id, @Column 등을 붙이기만 하면 된다.
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
}
JPA도 코드가 많다. 그래서 Spring이 제공하는 확장 라이브러리
Spring Data JPA 를 사용하면 Repository만 인터페이스로 정의해도 자동 구현된다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByName(String name);
}
이렇게 정의만 해두면,
Spring이 알아서 findByName에 맞는 SQL을 생성해 실행한다.
"서버 개발자"가 되기 위한 기본기를 다지기 위한 시간이었다.
단순히 CRUD를 만드는 걸 넘어서
"어떻게 잘 설계하고, 잘 동작하게 만들 것인가" 에 대한 고민을 시작할 수 있었다.