TL;DR

  • Backend 기본 개념의 핵심 개념과 사용 범위를 한눈에 정리
  • 등장 배경과 필요한 이유를 짚고 실무 적용 포인트를 연결
  • 주요 특징과 체크리스트를 빠르게 확인

1. 개념

POJO = Plain Old Java Object = 평범한 옛날 자바 객체

2. 배경

Backend 기본 개념이(가) 등장한 배경과 기존 한계를 정리한다.

3. 이유

  1. 테스트 용이성 - 프레임워크 없이 단위 테스트 가능

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