Backend 기본 개념
TL;DR
- Backend 기본 개념의 핵심 개념과 사용 범위를 한눈에 정리
- 등장 배경과 필요한 이유를 짚고 실무 적용 포인트를 연결
- 주요 특징과 체크리스트를 빠르게 확인
1. 개념
POJO = Plain Old Java Object = 평범한 옛날 자바 객체
2. 배경
Backend 기본 개념이(가) 등장한 배경과 기존 한계를 정리한다.
3. 이유
- 테스트 용이성 - 프레임워크 없이 단위 테스트 가능
4. 특징
- 약자
- 개념
- POJO vs Non-POJO
- 왜 POJO를 지향하는가?
- DTO vs Entity
5. 상세 내용
작성일: 2026-01-23 카테고리: Backend / Architecture 포함 내용: POJO, DTO, SSE, JPA/ORM, Hexagonal Architecture, DI/DIP, JWT, CORS
1. POJO (Plain Old Java Object)
약자
POJO = Plain Old Java Object = 평범한 옛날 자바 객체
개념
POJO = 특정 프레임워크에 종속되지 않은 순수한 자바/코틀린 객체
┌────────────────────────────────────────────────────────┐
│ POJO의 특징 │
│ │
│ 1. 특정 클래스를 상속받지 않음 │
│ 2. 특정 인터페이스를 구현하지 않음 │
│ 3. 특정 어노테이션에 종속되지 않음 │
│ │
│ 본질: 순수한 데이터 + 비즈니스 로직 │
└────────────────────────────────────────────────────────┘
POJO vs Non-POJO
// ✅ POJO - 순수한 코틀린 객체
data class User(
val id: Long,
val name: String,
val email: String
)
// ❌ Non-POJO - 프레임워크 종속
class UserEntity : BaseEntity(), Serializable {
@Column(name = "user_name")
var name: String = ""
}
왜 POJO를 지향하는가?
1. 테스트 용이성
- 프레임워크 없이 단위 테스트 가능
2. 유연성
- 프레임워크 교체 시 영향 최소화
3. 가독성
- 순수한 비즈니스 로직에 집중
2. DTO (Data Transfer Object)
약자
DTO = Data Transfer Object = 데이터 전송 객체
개념
DTO = 계층 간 데이터를 전달하는 목적의 객체
┌─────────────────────────────────────────────────────────┐
│ Controller ←──── DTO ────► Service ←──► Repository │
│ │ │ │
│ │ RequestDTO │ │
│ │◄────────────── │ │
│ │ │ │
│ │ ResponseDTO │ │
│ ├──────────────► │ │
└─────────────────────────────────────────────────────────┘
DTO vs Entity
// Entity - DB 테이블과 1:1 매핑 (내부용)
@Entity
data class UserEntity(
@Id val id: Long,
val name: String,
val password: String, // 민감 정보 포함
val createdAt: LocalDateTime
)
// DTO - API 응답용 (외부 노출)
data class UserResponse(
val id: Long,
val name: String
// password 제외!
)
왜 DTO를 사용하는가?
1. 보안: 민감한 정보 노출 방지
2. 성능: 필요한 데이터만 전송
3. 유연성: API 스펙과 DB 스키마 분리
4. 버전 관리: API 버전별 다른 DTO 사용 가능
3. SSE (Server-Sent Events)
약자
SSE = Server-Sent Events = 서버가 보내는 이벤트
개념
SSE = 서버 → 클라이언트 단방향 실시간 데이터 스트리밍
┌────────────────────────────────────────────────────────┐
│ │
│ Client Server │
│ │ │ │
│ │ HTTP 요청 (연결 시작) │ │
│ ├──────────────────────────────►│ │
│ │ │ │
│ │◄──────── 이벤트 1 ────────────│ │
│ │◄──────── 이벤트 2 ────────────│ │
│ │◄──────── 이벤트 3 ────────────│ │
│ │ ...계속 수신... │ │
│ │ │ │
└────────────────────────────────────────────────────────┘
특징: 한 번 연결하면 서버가 계속 데이터를 푸시
용도: 실시간 알림, 주식 시세, 채팅 등
SSE vs WebSocket
| 특성 | SSE | WebSocket |
|---|---|---|
| 방향 | 단방향 (서버→클라이언트) | 양방향 |
| 프로토콜 | HTTP | WS |
| 재연결 | 자동 | 수동 구현 |
| 브라우저 지원 | 좋음 | 좋음 |
| 용도 | 알림, 피드 | 채팅, 게임 |
Spring에서 SSE 구현
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun streamEvents(): Flux<ServerSentEvent<String>> {
return Flux.interval(Duration.ofSeconds(1))
.map { i ->
ServerSentEvent.builder<String>()
.id(i.toString())
.event("message")
.data("Event $i")
.build()
}
}
4. JPA와 ORM
약자
ORM = Object-Relational Mapping = 객체-관계형 매핑
JPA = Java Persistence API = 자바 영속성 API
개념
ORM = 객체(Object)와 관계형 DB(Relational)를 자동으로 매핑해주는 기술
┌─────────────────────────────────────────────────────────┐
│ │
│ Java/Kotlin 세계 DB 세계 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ User │ │ USERS │ │
│ │ - id: Long │◄─── ORM ──►│ - ID │ │
│ │ - name: Str │ │ - NAME │ │
│ └──────────────┘ └──────────────┘ │
│ 객체 테이블 │
│ │
└─────────────────────────────────────────────────────────┘
JPA는 ORM의 “표준 스펙”
ORM 구현체 종류:
├── Hibernate (가장 많이 사용)
├── EclipseLink
├── OpenJPA
└── ...
JPA = 이 구현체들이 따라야 할 표준 인터페이스
비유:
┌──────────────────────────────────────┐
│ JDBC = "DB 연결 표준" │
│ JPA = "ORM 표준" │
│ │
│ MySQL Driver : JDBC 구현체 │
│ Hibernate : JPA 구현체 │
└──────────────────────────────────────┘
ORM이 없던 시절
// 😱 JDBC 직접 사용
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
// ... 수동 매핑 지옥
ORM 사용 시
// 😊 JPA 사용
@Entity
data class User(
@Id val id: Long,
val name: String
)
// 한 줄로 조회
val user = userRepository.findById(id)
5. Hexagonal Architecture
개념
Hexagonal Architecture = 육각형 아키텍처 = Ports & Adapters
핵심 아이디어: 비즈니스 로직을 외부 의존성으로부터 보호
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Web │ │ DB │ │
│ │ Adapter │ │ Adapter │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Input │ │ │ │ Output │ │
│ │ Port ├─────►│ Domain │◄────┤ Port │ │
│ │ │ │ (Core) │ │ │ │
│ └─────────┘ └──────────┘ └─────────┘ │
│ │
│ Port = 인터페이스 (경계) │
│ Adapter = 실제 구현체 (외부 연결) │
│ Domain = 순수한 비즈니스 로직 │
│ │
└─────────────────────────────────────────────────────────┘
장점
1. 테스트 용이성
- 외부 의존성 없이 도메인 로직 테스트 가능
2. 유연성
- DB 교체, API 변경 시 Adapter만 수정
3. 관심사 분리
- 비즈니스 로직과 기술적 구현 분리
6. DI와 DIP
약자
DI = Dependency Injection = 의존성 주입
DIP = Dependency Inversion Principle = 의존성 역전 원칙
DI (의존성 주입)
의존성 주입 = 객체가 필요로 하는 의존성을 외부에서 넣어주는 것
┌─────────────────────────────────────────────────────────┐
│ │
│ ❌ 직접 생성 (강결합) │
│ class UserService { │
│ val repo = UserRepository() // 직접 생성 │
│ } │
│ │
│ ✅ 주입 받기 (약결합) │
│ class UserService( │
│ val repo: UserRepository // 외부에서 주입 │
│ ) │
│ │
└─────────────────────────────────────────────────────────┘
DIP (의존성 역전 원칙)
상위 모듈이 하위 모듈에 의존하면 안 됨
둘 다 추상화(인터페이스)에 의존해야 함
❌ 잘못된 의존 방향:
UserService → UserMySQLRepository
(구체 클래스에 직접 의존)
✅ 올바른 의존 방향:
UserService → UserRepository (인터페이스)
↑
UserMySQLRepository (구현체)
Spring에서의 DI
// 인터페이스 정의
interface UserRepository {
fun findById(id: Long): User?
}
// 구현체
@Repository
class UserMySQLRepository : UserRepository {
override fun findById(id: Long): User? = ...
}
// 사용 (인터페이스에 의존)
@Service
class UserService(
private val userRepository: UserRepository // 인터페이스 타입!
) {
fun getUser(id: Long) = userRepository.findById(id)
}
// Spring이 알아서 구현체(UserMySQLRepository)를 주입해줌
7. JWT (JSON Web Token)
약자
JWT = JSON Web Token = JSON 형식의 웹 토큰
개념
JWT = 정보를 JSON 형태로 담아 서명한 토큰
구조: xxxxx.yyyyy.zzzzz
Header.Payload.Signature
┌─────────────────────────────────────────────────────────┐
│ │
│ Header (헤더) │
│ { │
│ "alg": "HS256", // 서명 알고리즘 │
│ "typ": "JWT" // 토큰 타입 │
│ } │
│ │
│ Payload (페이로드) - 실제 데이터 │
│ { │
│ "sub": "user123", // 사용자 ID │
│ "name": "홍길동", │
│ "exp": 1234567890 // 만료 시간 │
│ } │
│ │
│ Signature (서명) - 위변조 방지 │
│ HMACSHA256( │
│ base64(header) + "." + base64(payload), │
│ secret_key │
│ ) │
│ │
└─────────────────────────────────────────────────────────┘
Session vs JWT
┌─────────────────────────────────────────────────────────┐
│ │
│ Session 방식 (Stateful) │
│ ├── 서버가 세션 정보를 메모리/DB에 저장 │
│ ├── 클라이언트는 Session ID만 가짐 │
│ └── 서버 확장 시 세션 공유 문제 │
│ │
│ JWT 방식 (Stateless) │
│ ├── 서버는 아무것도 저장 안 함 │
│ ├── 클라이언트가 모든 정보를 토큰에 담아 전송 │
│ └── 서버는 서명만 검증하면 됨 │
│ │
└─────────────────────────────────────────────────────────┘
JWT 인증 흐름
1. 로그인 요청
Client ──► Server: { email, password }
2. JWT 발급
Server ──► Client: { token: "eyJhbG..." }
3. 이후 모든 요청에 토큰 포함
Client ──► Server: Authorization: Bearer eyJhbG...
4. 서버는 토큰 검증만
Server: 서명 확인 → 유효하면 요청 처리
8. CORS (Cross-Origin Resource Sharing)
약자
CORS = Cross-Origin Resource Sharing = 교차 출처 리소스 공유
개념
CORS = 다른 출처(Origin)에서 리소스를 요청할 수 있게 허용하는 메커니즘
Origin = 프로토콜 + 도메인 + 포트
예시:
http://localhost:3000 ← Origin A
http://localhost:8080 ← Origin B (포트 다름 = 다른 출처!)
왜 필요한가? - SOP (Same-Origin Policy)
브라우저의 기본 보안 정책: SOP (동일 출처 정책)
"다른 출처의 리소스는 기본적으로 접근 불가!"
이유: 보안 (악의적인 사이트가 다른 사이트의 데이터 탈취 방지)
┌─────────────────────────────────────────────────────────┐
│ │
│ 악성 사이트 시나리오: │
│ │
│ 1. 사용자가 은행 사이트 로그인 (쿠키 저장) │
│ 2. 악성 사이트 방문 │
│ 3. 악성 사이트가 은행 API 호출 시도 │
│ 4. 브라우저가 차단! (SOP 덕분) │
│ │
└─────────────────────────────────────────────────────────┘
CORS 동작 방식
CORS = SOP의 예외를 허용하는 메커니즘
서버가 "이 출처는 허용한다"고 응답 헤더로 알려줌:
Access-Control-Allow-Origin: http://localhost:3000
┌─────────────────────────────────────────────────────────┐
│ │
│ 브라우저 (localhost:3000) │
│ │ │
│ │ 1. API 요청 │
│ ├────────────────────► 서버 (localhost:8080) │
│ │ │ │
│ │ 2. 응답 + CORS 헤더 │ │
│ │◄──────────────────── │ │
│ │ │ │
│ │ Access-Control-Allow-Origin:│ │
│ │ http://localhost:3000 │ │
│ │ │ │
│ 3. 브라우저가 CORS 헤더 확인 │
│ └── 허용 목록에 있으면 → 응답 사용 │
│ └── 없으면 → 차단! │
│ │
└─────────────────────────────────────────────────────────┘
💡 핵심: 서버는 정상 응답함! 브라우저가 차단하는 것!
실제 사례
시나리오:
- 프론트엔드: http://192.168.11.65:3000
- 백엔드: http://192.168.11.65:8072
- 백엔드 CORS 설정: localhost:8072만 허용
결과: 브라우저에서 접근 불가!
이유: 192.168.11.65 ≠ localhost (다른 Origin)
Spring에서 CORS 설정
@Configuration
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins(
"http://localhost:3000",
"http://192.168.11.65:3000" // IP도 추가!
)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
}
}
관련 키워드
POJO, DTO, SSE, JPA, ORM, Hibernate, Hexagonal Architecture, Ports and Adapters, DI, DIP, SOLID, JWT, JSON Web Token, CORS, SOP, Cross-Origin