자바 record·Optional·애노테이션: 프론트 개발자가 자바 코드를 읽기 시작할 때 (문법편)

자바 record와 Optional, 애노테이션이 뭔지, Entity와 DTO를 나누는 이유를 TypeScript에 빗대 정리했다. 프론트 개발자가 스프링 코드를 읽기 시작할 때 필요한 문법만.

조회
목차

프론트만 하다가 백엔드 코드를 직접 고쳐야 하는 작업이 들어왔다.

로직은 이해되는데 문법이 안 읽혔다. 뭘 하는 코드인지는 알겠는데 @ 붙은 줄들이 뭔지, 왜 파일이 다섯 개나 되는지, new를 안 쓰는데 객체가 어디서 오는지를 몰랐다.

자바 강의를 처음부터 듣기엔 급하고, 검색하면 나오는 건 실무 코드와 모양이 달랐다. 실제로 고쳐야 하는 코드에 나온 것만 골라서 정리했다. TypeScript를 아는 상태 전제.

먼저 — 왜 파일이 이렇게 많은가

기능 하나에 파일이 다섯 개쯤 생긴다. 처음엔 이게 제일 당황스럽다.

Controller   요청을 받는다. 검증하고 Service 를 부른다.
Service      실제 로직. 여기가 본체.
Repository   DB 접근. SQL 이 여기 붙는다.
Entity       DB 테이블 한 줄에 대응하는 객체.
DTO          API 로 주고받는 모양.

프론트로 치면 route handler → 비즈니스 로직 → API 클라이언트를 파일로 강제 분리한 거다.

나누는 이유는 각 층이 아는 것이 다르기 때문이다.

  • Controller는 HTTP를 안다. DB는 모른다.
  • Service는 도메인 규칙을 안다. HTTP도 DB도 모른다.
  • Repository는 DB만 안다.

그래서 "SQL을 어디서 고치지" 같은 질문의 답이 항상 정해져 있다.

Entity와 DTO를 왜 나누나

같아 보이는 객체가 두 개 있어서 헷갈린다.

EntityDTO
뭐냐DB 테이블 한 줄API 요청/응답 모양
바뀌는 이유스키마가 바뀔 때API 계약이 바뀔 때
담기는 것컬럼 전부 (비밀번호 해시 포함)밖에 보여줄 것만

Entity를 그대로 응답으로 내보내면 안 되는 이유가 이거다. 컬럼 하나 추가했을 뿐인데 API 응답이 같이 바뀌고, 내부 값이 밖으로 샌다.

record — DTO는 거의 다 이것

public record RegisterRequest(
    String userId,
    String email
) {}

TypeScript의 type + 값이 안 바뀌는 객체다. 이 한 줄이 생성자·getter·비교 함수를 다 만들어준다.

var req = new RegisterRequest("hong", "a@b.com");
req.userId();     // getter 는 get 접두사 없이 필드명 그대로
// req.userId = "x";   ← 불가능. 만들고 나면 안 바뀐다

값이 안 바뀌는 게 핵심이다. 요청 객체가 중간에 바뀌면 추적이 안 되니까 아예 막는다.

옛날 방식으로 같은 걸 하려면 40줄쯤 된다. 필드마다 getter/setter를 손으로 쓴다. 그 노가다에서 실수가 난다 — 복사해놓고 필드명 안 바꾸는 것 같은. record가 그래서 생겼다.

Optional — "없을 수도 있음"을 강제로 열게 함

Optional<UserEntity> found = repository.findById(seq);

TypeScript의 User | undefined인데, 그냥 못 쓰고 반드시 열어야 한다.

// 없으면 예외 (제일 많이 쓴다)
var user = repository.findById(seq)
    .orElseThrow(() -> new ApiException("사용자가 없습니다"));

// 없으면 그냥 끝
if (found.isEmpty()) return;
var user = found.get();

TypeScript의 ?.는 안 열어도 통과하지만 Optional안 열면 컴파일이 안 된다. null을 깜빡하는 걸 문법으로 막는 장치다.

애노테이션 — 제일 헷갈렸던 것

@ 붙은 줄이 사방에 있는데, 이건 자바 문법이 아니라 프레임워크가 읽는 메모다.

@RestController                      // Spring: 이 클래스는 HTTP 요청을 받는다
@RequestMapping("/api/user")
public class UserController {

    @GetMapping("/{seq}")            // Spring: GET /api/user/{seq} 를 이 메서드로
    public UserResponse get(@PathVariable int seq) { ... }
}

핵심은 이거다 — 애노테이션 자체는 아무 일도 안 한다. 그냥 표시다. 프레임워크가 시작할 때 클래스들을 훑어보고, 그 표시를 발견하면 그때 뭔가를 한다.

TypeScript 데코레이터와 같은 개념인데 자바에선 훨씬 많이 쓴다.

애노테이션누가 읽나무슨 뜻
@RestController @ServiceSpring이 클래스를 관리 대상으로 등록
@GetMapping @PostMappingSpringURL ↔ 메서드 연결
@TransactionalSpring이 메서드를 트랜잭션으로 감싼다
@NotBlank @Email검증기요청 값 규칙
@MapperMyBatis이 인터페이스를 SQL과 연결
@Test @Mock테스트 도구테스트 메서드 / 가짜 객체

그 외 짧은 것들

var — 타입 추론. TypeScript와 거의 같다.

final — 재할당 금지. const와 비슷한데 자바에선 필드에 많이 붙는다.

enum — 정해진 값 목록. 문자열 대신 쓰면 오타를 컴파일 때 잡아준다.

제네릭 <> — TypeScript와 같다.

지금은 안 배워도 되는 것

시간이 한정돼 있으니 미룰 것도 정해두는 게 낫다.

  • 상속·추상클래스·다형성 — 실무 스프링 코드에선 생각보다 안 쓴다
  • 스레드·동시성 — 프레임워크가 처리한다
  • 자바 8 이전 문법 — 요즘 코드엔 잘 안 나온다
  • Gradle 심화 — 빌드가 깨졌을 때만 보면 된다

남길 것

  • 파일이 여러 개인 건 각 층이 아는 것이 다르기 때문이다. 그래서 "어디를 고치지"의 답이 정해져 있다.
  • Entity와 DTO를 나누는 이유는 "바뀌는 이유가 다르기" 때문이다.
  • 애노테이션은 문법이 아니라 메모다. 지워도 컴파일은 되고 동작만 바뀐다.
  • 읽기 연습은 이미 고쳐본 파일로 한다. 뭘 하는 코드인지 아는 상태에서 문법만 붙이면 되니까 훨씬 빠르다. 모르는 코드로 시작하면 로직과 문법을 동시에 풀어야 한다.

다음 글에서 Spring과 MyBatis, 테스트 코드를 정리한다.

사이트 내 검색

제목·태그·설명·본문으로 발행 글을 찾습니다