Backend 기본 개념
TL;DR
- 이 글은 백엔드에서 반복해서 마주치는 핵심 용어와 설계 축을 한 번에 묶어 설명한다.
- 데이터 표현, 계층 분리, 실시간 통신, 영속성, 의존성 관리, 인증·브라우저 보안까지 연결해서 이해하는 데 초점을 둔다.
- 원문 전체는 아래 상세 내용에 그대로 포함했다.
1. 개념
Backend 기본 개념은 서버 애플리케이션을 구성하는 대표 용어를 묶어, 객체 모델링·데이터 전달·실시간 통신·영속성·아키텍처·보안의 역할 분담을 빠르게 이해하도록 돕는 기초 지식 묶음이다.
2. 배경
실제 백엔드 개발은 단순히 API를 만드는 일을 넘어서 도메인 객체, DTO 분리, ORM 선택, 의존성 역전, 인증 토큰, 브라우저 보안 정책까지 동시에 다뤄야 한다. 그래서 개별 기술을 따로 외우기보다 각 개념이 어떤 계층 문제를 해결하는지 먼저 잡아두는 공통 프레임이 필요하다.
3. 이유
이런 기본 개념을 먼저 정리해두면 POJO와 DTO를 구분해 모델 경계를 깔끔하게 유지하고, SSE와 같은 통신 방식을 상황에 맞게 선택하며, JPA/ORM과 DI/DIP를 통해 유지보수성과 테스트 용이성을 높일 수 있다. 동시에 JWT와 CORS처럼 운영 단계에서 자주 부딪히는 인증·브라우저 제약도 더 자연스럽게 이해할 수 있다.
4. 특징
- 객체 모델링 관점: POJO와 DTO를 대비해 순수 도메인 모델과 전송 모델의 경계를 설명한다.
- 통신 관점: SSE를 통해 실시간 단방향 스트리밍이 WebSocket과 어떻게 다른지 보여준다.
- 영속성 관점: JPA와 ORM의 관계, 표준과 구현체의 차이를 함께 정리한다.
- 설계 관점: Hexagonal Architecture, DI, DIP를 통해 결합도와 테스트 가능성을 다룬다.
- 보안 관점: JWT와 CORS를 통해 인증 정보 전달과 브라우저 정책의 핵심을 묶어 설명한다.
5. 상세 내용
Backend 기본 개념
작성일: 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