← DoniKit 웹 앱으로

Java 코드 스니펫 39종

SI 실무에서 자주 쓰는 Java 코드 조각 모음입니다. 컬렉션·Stream·형변환(VO↔Map↔JSON)·날짜 비교·예외 처리·JUnit/Mockito 테스트·HTTP 클라이언트(JDK 11+와 JDK 8+ 레거시)·파일 처리까지, 매번 검색하게 되는 정형 코드를 완성형으로 정리했습니다.

웹 앱에서는 빈칸을 채워 완성된 코드를 바로 복사할 수 있습니다.

반복문 3종 (for · for-each · Stream) + Map 순회

반복문

List<String> items = List.of("a", "b", "c");

// 1) 기본 for (인덱스 필요할 때)
for (int i = 0; i < items.size(); i++) {
    System.out.println(i + ": " + items.get(i));
}

// 2) 향상된 for (for-each) — 값만 필요할 때
for (String item : items) {
    System.out.println(item);
}

// 3) Stream
items.forEach(System.out::println);

// Map 순회 (entrySet)
Map<String, Integer> scores = Map.of("kim", 90, "lee", 80);
for (Map.Entry<String, Integer> e : scores.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
}

List<Map<String, Object>> 생성 · 데이터 넣기

컬렉션

List<Map<String, Object>> rows = new ArrayList<>();

// 한 건씩 put
Map<String, Object> row = new HashMap<>();
row.put("id", 1);
row.put("name", "홍길동");
row.put("active", true);
rows.add(row);

// 불변 맵으로 바로 추가 (Java 9+)
rows.add(Map.of("id", 2, "name", "김철수", "active", false));

// 읽기
for (Map<String, Object> r : rows) {
    System.out.println(r.get("id") + " / " + r.get("name"));
}

💡 Map.of(...)는 불변(수정·null 불가). 값을 넣었다 뺐다 하려면 new HashMap<>()을 쓴다.

List · Set · Map 초기화 (가변 vs 불변)

컬렉션

// List
List<String> list  = new ArrayList<>();             // 가변(추가·삭제 가능)
List<String> fixed = List.of("a", "b", "c");        // 불변 (Java 9+)
List<String> copy  = new ArrayList<>(List.of("a", "b")); // 불변→가변 복사

// Set (중복 제거)
Set<String> set = new HashSet<>(List.of("a", "b", "a")); // {a, b}

// Map
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
Map<String, Integer> imap = Map.of("a", 1, "b", 2);  // 불변

문자열 ↔ 숫자 형변환

형변환

// 문자열 → 숫자
int i    = Integer.parseInt("42");
long l   = Long.parseLong("42");
double d = Double.parseDouble("3.14");

// 숫자 → 문자열
String s1 = String.valueOf(42);
String s2 = Integer.toString(42);

// 실패 대비 — 숫자가 아니면 NumberFormatException
try {
    int n = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
    // 기본값·오류 처리
}

💡 parseXxx는 숫자가 아니면 예외를 던진다. 사용자 입력엔 try/catch가 사실상 필수.

VO ↔ Map<String, Object> (리스트 포함)

형변환 · Jackson 2 (ObjectMapper)

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
UserVo vo = new UserVo();

// VO → Map
Map<String, Object> map = mapper.convertValue(vo, new TypeReference<Map<String, Object>>() {});

// Map → VO
UserVo back = mapper.convertValue(map, UserVo.class);

// List<VO> → List<Map>  (MyBatis·JDBC 결과와 주고받을 때)
List<UserVo> voList = List.of(vo); // Java 9+ · Java 8이면 Arrays.asList(vo)
List<Map<String, Object>> rows =
        mapper.convertValue(voList, new TypeReference<List<Map<String, Object>>>() {});

// List<Map> → List<VO>
List<UserVo> parsed = mapper.convertValue(rows, new TypeReference<List<UserVo>>() {});

💡 convertValue는 JSON 문자열을 거치지 않고 객체↔Map을 바로 바꾼다(직렬화보다 빠름). VO에 기본 생성자와 getter/setter(또는 record)가 없으면 InvalidDefinitionException.

VO ↔ JSON 문자열 (배열 → List<VO>)

형변환 · Jackson 2 (ObjectMapper)

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
UserVo vo = new UserVo();

// VO → JSON 문자열
String json = mapper.writeValueAsString(vo);

// 보기 좋게 들여쓰기해서
String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(vo);

// JSON 문자열 → VO
UserVo back = mapper.readValue(json, UserVo.class);

// JSON 배열 → List<VO>  (제네릭은 TypeReference로 알려줘야 한다)
String arrayJson = "[{\"id\":1},{\"id\":2}]";
List<UserVo> list = mapper.readValue(arrayJson, new TypeReference<List<UserVo>>() {});

💡 readValue·writeValueAsString은 검사예외(JsonProcessingException)를 던진다. JSON에 VO가 모르는 필드가 하나라도 있으면 기본 설정에선 UnrecognizedPropertyException — 아래 "ObjectMapper 실무 설정" 카드 참고.

Map ↔ JSON 문자열

형변환 · Jackson 2 (ObjectMapper)

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();

// Map → JSON 문자열
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "홍길동");
String json = mapper.writeValueAsString(map);

// JSON 문자열 → Map
Map<String, Object> back = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});

// 값은 Object로 들어있어 캐스팅이 필요하다
String name = (String) back.get("name");
Integer id = (Integer) back.get("id");

💡 JSON→Map은 값이 전부 Object라 캐스팅이 필요하고, 숫자는 크기에 따라 Integer·Long·Double로 달라져 ClassCastException이 나기 쉽다. 타입이 중요하면 Map 대신 VO로 받는다.

ObjectMapper 실무 설정 (안 하면 터지는 것들)

형변환 · Jackson 2 (ObjectMapper)

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

ObjectMapper mapper = new ObjectMapper();

// 1) VO에 없는 필드가 와도 무시 — 서버가 응답 필드를 추가해도 안 터진다
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 2) 값이 null인 필드는 JSON에서 아예 빼기
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

// 3) LocalDate·LocalDateTime 지원 — 등록 안 하면 직렬화에서 예외
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 1720512000 → "2026-07-09T15:00:00"

// 4) DB 컬럼(user_name) ↔ 자바 필드(userName) 자동 매핑
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

💡 1번이 실무 1순위 — 없으면 상대 API가 필드 하나만 추가해도 UnrecognizedPropertyException으로 죽는다. JavaTimeModule은 별도 의존성(jackson-datatype-jsr310), PropertyNamingStrategies는 Jackson 2.12+(이전엔 PropertyNamingStrategy). Spring Boot는 이 대부분을 자동으로 해준다.

돈 계산 BigDecimal (double 함정 · 반올림 · 비교)

형변환

import java.math.BigDecimal;
import java.math.RoundingMode;

// double로 돈 계산 금지 — 이진 오차
System.out.println(0.1 + 0.2); // 0.30000000000000004

// 생성은 "문자열"로 — new BigDecimal(0.1)은 그 오차까지 그대로 담는다
BigDecimal price = new BigDecimal("10000");
BigDecimal rate  = new BigDecimal("0.135");

// 사칙연산 — 불변 객체라 반환값을 받아야 한다
BigDecimal fee   = price.multiply(rate); // 1350.000
BigDecimal total = price.add(fee);

// 나눗셈은 자리수·반올림을 반드시 지정 (무한소수면 예외)
BigDecimal share = total.divide(new BigDecimal("3"), 0, RoundingMode.HALF_UP);

// 반올림 · 버림
BigDecimal rounded = fee.setScale(0, RoundingMode.HALF_UP); // 1350
BigDecimal cut     = fee.setScale(0, RoundingMode.DOWN);    // 1350

// 비교는 equals가 아니라 compareTo — 스케일(1.0 vs 1) 차이를 무시
boolean same = new BigDecimal("1.0").compareTo(new BigDecimal("1")) == 0; // true (equals는 false)

💡 divide는 나누어떨어지지 않으면 ArithmeticException — 스케일·RoundingMode 필수. setScale도 새 객체를 반환한다(원본 불변). 표시용 문자열은 toPlainString()(지수 표기 방지).

문자열 ↔ 날짜 (LocalDate · LocalDateTime)

날짜·시간

import java.time.*;
import java.time.format.DateTimeFormatter;

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 문자열 → 날짜
LocalDateTime dt = LocalDateTime.parse("2026-07-07 21:30:00", fmt);
LocalDate date   = LocalDate.parse("2026-07-07"); // ISO 기본형은 포맷 불필요

// 날짜 → 문자열
String text = dt.format(fmt);

💡 java.util.Date/SimpleDateFormat 대신 java.time(JSR-310) 권장 — 불변·스레드 안전.

HTTP GET 요청

HTTP 클라 · JDK 11+ (java.net.http)

import java.net.URI;
import java.net.http.*;

HttpClient client = HttpClient.newHttpClient();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/users?id=1"))
        .header("Accept", "application/json")
        .GET()
        .build();

HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode());
System.out.println(res.body());

💡 send(...)는 검사예외(IOException·InterruptedException)를 던지므로 throws 또는 try/catch 필요. java.net.http는 JDK 11부터라 Java 8에선 아래 HttpURLConnection 카드를 쓴다.

HTTP POST 요청 (JSON 전송)

HTTP 클라 · JDK 11+ (java.net.http)

import java.net.URI;
import java.net.http.*;

String json = "{\"name\":\"홍길동\",\"age\":30}";

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/users"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

HttpResponse<String> res = HttpClient.newHttpClient()
        .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());

💡 실무에선 문자열 직접 조립 대신 Jackson(ObjectMapper)으로 객체→JSON 직렬화를 권장.

HTTP GET (타임아웃 · 헤더 · 토큰 인증)

HTTP 클라 · JDK 11+ (java.net.http)

import java.net.URI;
import java.net.http.*;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5)) // 연결까지 5초 — 클라이언트에 건다
        .build();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/me"))
        .timeout(Duration.ofSeconds(10))       // 응답까지 10초 — 요청에 건다
        .header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.xxx.yyy")
        .header("Accept", "application/json")
        .GET()
        .build();

HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());

if (res.statusCode() == 401) {
    System.out.println("인증 실패 — 토큰이 없거나 만료됨");
} else {
    System.out.println(res.statusCode() + " " + res.body());
}

💡 타임아웃은 자리가 둘로 나뉜다 — 연결 대기는 HttpClient.connectTimeout, 응답 대기는 HttpRequest.timeout. 응답 타임아웃 초과 시 HttpTimeoutException.

HTTP 비동기 요청 (sendAsync · 기다리지 않고 진행)

HTTP 클라 · JDK 11+ (java.net.http)

import java.net.URI;
import java.net.http.*;
import java.util.concurrent.CompletableFuture;

HttpClient client = HttpClient.newHttpClient();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/users"))
        .GET()
        .build();

// 보내자마자 즉시 반환된다 — 응답을 기다리지 않는다
CompletableFuture<String> future = client
        .sendAsync(req, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body);

System.out.println("요청 보냄. 응답 오는 동안 다른 일 처리...");

// 결과가 필요한 지점에서 꺼낸다 — 여기서 비로소 기다린다
String body = future.join();
System.out.println(body);

💡 sendAsync는 검사예외를 던지지 않는다 — 실패는 CompletableFuture 안에 담겨 join() 시점에 CompletionException으로 터진다. join() 없이 main이 먼저 끝나면 응답을 못 본다.

쿼리스트링 안전 조립 (URLEncoder · 한글/특수문자)

HTTP 클라 · JDK 11+ (java.net.http)

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;

String keyword = "커피 머신 & 원두";

// 값마다 인코딩한다 — 통째로 인코딩하면 =·&까지 망가진다
String query = "keyword=" + URLEncoder.encode(keyword, StandardCharsets.UTF_8)
        + "&page=1";

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/search?" + query))
        .GET()
        .build();

HttpResponse<String> res = HttpClient.newHttpClient()
        .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());

💡 인코딩을 빠뜨리면 한글·공백·&가 든 값에서 파라미터가 잘린다. URLEncoder는 공백을 +로 바꾸므로 쿼리스트링 값 전용 — 경로(/a/b)에는 쓰면 안 된다.

HTTP GET 요청 (구버전 JDK)

HTTP 클라 · JDK 8+ (HttpURLConnection)

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

HttpURLConnection con = (HttpURLConnection) new URL("https://api.example.com/users?id=1").openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Accept", "application/json");
con.setConnectTimeout(5000); // 연결 대기 5초 (기본값은 무한 대기)
con.setReadTimeout(5000);    // 응답 대기 5초

try {
    int status = con.getResponseCode();
    // 400 이상이면 getInputStream()이 예외를 던진다 → 에러 본문은 getErrorStream()으로
    InputStream stream = (status >= 400) ? con.getErrorStream() : con.getInputStream();

    StringBuilder body = new StringBuilder();
    if (stream != null) {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
            String line;
            while ((line = br.readLine()) != null) {
                body.append(line);
            }
        }
    }
    System.out.println(status + " " + body);
} finally {
    con.disconnect();
}

💡 타임아웃 기본값이 무한 대기라 set*Timeout은 사실상 필수. 상태코드 400 이상은 getInputStream()이 예외를 던지므로 getErrorStream()으로 읽는다(에러 본문이 없으면 null).

HTTP POST 요청 (구버전 JDK · JSON 전송)

HTTP 클라 · JDK 8+ (HttpURLConnection)

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

String json = "{\"name\":\"홍길동\",\"age\":30}";

HttpURLConnection con = (HttpURLConnection) new URL("https://api.example.com/users").openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
con.setConnectTimeout(5000);
con.setReadTimeout(5000);
con.setDoOutput(true); // 본문을 보내려면 반드시 켠다

try {
    try (OutputStream os = con.getOutputStream()) {
        os.write(json.getBytes("UTF-8"));
    }

    int status = con.getResponseCode();
    InputStream stream = (status >= 400) ? con.getErrorStream() : con.getInputStream();

    StringBuilder body = new StringBuilder();
    if (stream != null) {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
            String line;
            while ((line = br.readLine()) != null) {
                body.append(line);
            }
        }
    }
    System.out.println(status + " " + body);
} finally {
    con.disconnect();
}

💡 setDoOutput(true)를 빠뜨리면 본문이 전송되지 않는다. getBytes()에 인코딩을 안 주면 OS 기본 문자셋이 쓰여 한글이 깨진다.

컨트롤러로 GET/POST 받기 (@RestController)

HTTP 서버 · Spring Boot 3

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    // GET /api/users/1
    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return userService.findById(id);
    }

    // GET /api/users?keyword=kim
    @GetMapping
    public List<User> search(@RequestParam(required = false) String keyword) {
        return userService.search(keyword);
    }

    // POST /api/users  (JSON 바디 → 객체)
    @PostMapping
    public User create(@RequestBody UserRequest body) {
        return userService.create(body);
    }
}

💡 @PathVariable=경로 변수, @RequestParam=쿼리스트링(?a=b), @RequestBody=JSON 바디를 객체로.

내장 HTTP 서버 (프레임워크 없이 · 테스트 목업용)

HTTP 서버 · JDK 8+ (com.sun.net.httpserver)

import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

server.createContext("/api/health", exchange -> {
    String method = exchange.getRequestMethod(); // GET · POST ...
    String body = "{\"status\":\"ok\",\"method\":\"" + method + "\"}";
    byte[] bytes = body.getBytes(StandardCharsets.UTF_8);

    exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
    exchange.sendResponseHeaders(200, bytes.length); // 두 번째 인자 = 본문 바이트 길이

    try (OutputStream os = exchange.getResponseBody()) {
        os.write(bytes);
    }
});

server.setExecutor(null); // 기본 실행기(단일 스레드) — 목업이면 충분
server.start();
System.out.println("대기 중: http://localhost:8080/api/health");

💡 JDK에 들어있지만 Java SE 표준 규격은 아니다(jdk.httpserver 모듈) — 상대 API가 아직 없을 때 띄우는 테스트 목업·사내 도구용이고 프로덕션 서버로는 쓰지 않는다. 응답 길이를 모르면 sendResponseHeaders(200, 0)으로 청크 전송.

Stream 걸러서 뽑기 (filter · map · collect)

컬렉션 가공

// 걸러서 이름만 뽑아 리스트로
List<String> names = users.stream()
        .filter(u -> u.getAge() >= 20)
        .map(User::getName)
        .collect(Collectors.toList());   // Java 16+: .toList()

// 개수 · 존재 여부
long adults = users.stream().filter(u -> u.getAge() >= 20).count();
boolean hasKim = users.stream().anyMatch(u -> u.getName().equals("김철수"));

// 합계
int total = users.stream().mapToInt(User::getAge).sum();

💡 import static java.util.stream.Collectors.*; 하면 toList()/joining() 등을 짧게 쓸 수 있다.

Stream 그룹핑 · 정렬 · 이어붙이기

컬렉션 가공

// 조건별 그룹핑 → Map<City, List<User>>
Map<String, List<User>> byCity = users.stream()
        .collect(Collectors.groupingBy(User::getCity));

// 나이 오름차순, 같으면 이름순
List<User> sorted = users.stream()
        .sorted(Comparator.comparingInt(User::getAge)
                          .thenComparing(User::getName))
        .collect(Collectors.toList());

// 콤마로 이어붙이기 → "홍길동, 김철수"
String csv = users.stream().map(User::getName)
        .collect(Collectors.joining(", "));

문자열 다루기 (자르기 · 나누기 · 치환)

문자열

String s = "  Hello, World  ";

s.trim();               // 앞뒤 공백 제거
s.strip();              // 유니코드 공백까지 (Java 11+)
s.toLowerCase();
s.contains("World");    // true
s.startsWith("  He");   // true
s.replace("l", "L");    // 모두 치환

// 자르기 · 나누기 · 잇기
String[] parts = "a,b,c".split(",");      // [a, b, c]
String joined = String.join("-", parts);  // "a-b-c"
String sub = "abcdef".substring(1, 4);    // "bcd"

문자열 포맷 · 조립 (String.format · StringBuilder)

문자열

// 포맷 (%s 문자열, %d 정수, %,.0f 천단위+소수0)
String msg = String.format("%s님 %d세, 잔액 %,.0f원", "홍길동", 30, 15000.0);

// 반복문 안 이어붙이기는 StringBuilder (+ 반복은 느림)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++) {
    sb.append("row").append(i).append("\n");
}
String result = sb.toString();

// 반복 · 빈 문자열 검사 (Java 11+)
String line = "=".repeat(20);
boolean blank = "  ".isBlank();   // true

정규식 (Pattern · Matcher — 검증 · 추출 · 치환)

문자열

// 검증 — 문자열 "전체"가 패턴과 일치하는가
boolean ok = Pattern.matches("\\d{3}-\\d{4}", "010-1234"); // true

// 반복 사용은 컴파일해 재사용 (성능)
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher("시작 2026-07-10 종료 2026-08-01");

// 추출 — 찾을 때마다 group
while (m.find()) {
    System.out.println(m.group());  // 2026-07-10 (전체 일치분)
    System.out.println(m.group(1)); // 2026 (1번 괄호)
}

// 치환 — $n으로 잡은 그룹 재사용 (전화번호 마스킹)
String masked = "010-1234-5678"
    .replaceAll("(\\d{3})-(\\d{4})-(\\d{4})", "$1-****-$3");

💡 matches()는 전체 일치, find()는 부분 검색 — 헷갈리면 검증이 전부 실패한다. 자바 문자열 안에선 \d를 \\d처럼 역슬래시 두 번으로 쓴다.

날짜 연산 (현재·더하기·차이·비교)

날짜·시간

import java.time.*;
import java.time.temporal.ChronoUnit;

LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();

// 더하기 · 빼기 (불변 — 새 객체 반환)
LocalDate nextWeek = today.plusDays(7);
LocalDate lastMonth = today.minusMonths(1);

// 두 날짜 차이
long days = ChronoUnit.DAYS.between(today, nextWeek);   // 7
Duration dur = Duration.between(now, now.plusHours(3)); // PT3H

// 비교
boolean before = today.isBefore(nextWeek);              // true

날짜 비교 — 기준 형태로 완성해서 비교 (yyyyMMdd · yyyyMMddHHmmss · LocalDateTime · LocalDate)

날짜·시간

import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;

// 쓰는 법: 비교할 기준 형태를 하나 고르고, 그 영역의 변환으로 값을 완성한 뒤 비교한다.
// 예시 값: vo.getRegDt() = DB에서 온 Date·Timestamp (안의 값: 2026-07-30 14:30:25)

// ══ 1. "yyyyMMdd" 문자열로 완성해서 비교 — 완성형: "20260730" ══════
String s1 = "20260730143025".substring(0, 8);                               // "20260730143025" → "20260730"
String s2 = new SimpleDateFormat("yyyyMMdd").format(vo.getRegDt());  // Date(2026-07-30 14:30:25) → "20260730"
String s3 = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 오늘 → "20260802"

boolean same  = s1.equals(s2);        // "20260730".equals("20260730") → true
boolean later = s1.compareTo(s2) > 0; // 이후인가 (같은 자릿수 문자열은 사전순 = 시간순)

// ══ 2. "yyyyMMddHHmmss" 문자열로 완성해서 비교 — 완성형: "20260730143025" ══
String t1 = "20260730" + "000000";                                          // "20260730" → "20260730000000" (자정 붙이기)
String t2 = new SimpleDateFormat("yyyyMMddHHmmss").format(vo.getRegDt()); // Date(2026-07-30 14:30:25) → "20260730143025"
String t3 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); // 지금 → "20260802093000"

boolean sameSec = t1.equals(t2);        // "…000000" vs "…143025" → false (초까지 비교)
boolean before  = t1.compareTo(t2) < 0; // 자정 < 14시 30분 → true

// ══ 3. LocalDateTime(시각 포함 타입)으로 완성해서 비교 — 완성형: 2026-07-30T14:30:25 ══
LocalDateTime m1 = LocalDateTime.parse("20260730143025",                  // "20260730143025" → 2026-07-30T14:30:25
    DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
LocalDateTime m2 = vo.getRegDt().toInstant()                       // Date → 2026-07-30T14:30:25
    .atZone(ZoneId.systemDefault()).toLocalDateTime();

boolean eq    = m1.isEqual(m2);
boolean after = m1.isAfter(m2);

// ══ 4. LocalDate(날짜 타입)로 완성해서 비교 — 완성형: 2026-07-30 ══
LocalDate d1 = LocalDate.parse("20260730", DateTimeFormatter.ofPattern("yyyyMMdd")); // "20260730" → 2026-07-30
LocalDate d2 = m2.toLocalDate();                                   // 2026-07-30T14:30:25 → 2026-07-30 (시각 버림)

boolean sameDay = d1.isEqual(d2);   // 같은 날인가 → true
boolean earlier = d1.isBefore(d2);

💡 기준 고르는 법 — 값이 JS·DB 등 층을 넘어가 비교되면 문자열(1·2번, 어디로 가도 그대로), 자바 안에서 날짜 연산(더하기·차이)까지 하면 타입(3·4번). equals·compareTo는 자릿수가 같을 때만 성립 — 8자리와 14자리를 섞어 비교하면 어긋난다. new SimpleDateFormat은 지역 변수로 쓰면 안전(static 공유가 스레드 함정).

레거시 Date · Timestamp ↔ java.time ↔ 문자열 (MyBatis/JDBC 정합)

날짜·시간 · JDK 8+

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;

DateTimeFormatter D14 = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

// VO에 java.util.Date / java.sql.Timestamp로 담겨 오는 값 (MyBatis·JDBC 기본)
Date legacy = vo.getRegDt();
java.sql.Timestamp ts = rs.getTimestamp("reg_dt");

// 1) 레거시 → java.time (변환 후 비교·연산)
LocalDateTime fromDate = legacy.toInstant()
    .atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime fromTs = ts.toLocalDateTime(); // Timestamp는 전용 메서드가 있다

// 2) java.time → 레거시 (구식 API에 넘겨줄 때)
Date toDate = Date.from(fromDate.atZone(ZoneId.systemDefault()).toInstant());
java.sql.Timestamp toTs = java.sql.Timestamp.valueOf(fromDate);

// 3) 레거시 ↔ 문자열은 java.time을 거친다
String s14 = fromTs.format(D14);
Date parsed = Date.from(LocalDateTime.parse("20260730143025", D14)
    .atZone(ZoneId.systemDefault()).toInstant());

// 4) 비교 — 어느 한쪽으로 통일한 뒤
boolean same = fromDate.isEqual(fromTs);
boolean newer = legacy.toInstant().isAfter(ts.toInstant()); // Instant끼리도 가능

💡 SimpleDateFormat은 스레드 안전하지 않아 static으로 두면 동시 요청에서 값이 섞인다 — 문자열 변환은 java.time을 거치는 쪽이 안전. java.sql.Date는 시각이 없고(toLocalDate만), java.util.Date와 이름만 같은 다른 타입이라 import 충돌에 주의(위처럼 한쪽을 풀네임으로).

스케줄러 @Scheduled (주기 · 크론 배치)

날짜·시간 · Spring Boot 3

@Configuration
@EnableScheduling // 이게 없으면 @Scheduled가 조용히 무시된다
public class SchedulerConfig {}

@Component
public class BatchJobs {

    // 고정 주기 — 이전 실행 "시작" 기준 5분마다
    @Scheduled(fixedRate = 300000)
    public void poll() { /* 주기 폴링 */ }

    // 고정 간격 — 이전 실행이 "끝난 뒤" 5분 쉬고
    @Scheduled(fixedDelay = 300000)
    public void cleanup() { /* 뒷정리 배치 */ }

    // 크론 — 예: 매일 새벽 4시 30분 (초 분 시 일 월 요일 = 6칸)
    @Scheduled(cron = "0 30 4 * * *")
    public void dailyReport() { /* 일 배치 */ }
}

💡 Spring 크론은 6칸(초 포함) — 리눅스 5칸을 그대로 붙이면 시작부터 실패한다. 기본 스케줄러 스레드는 1개라 한 작업이 길어지면 다른 작업이 밀린다(spring.task.scheduling.pool.size). 크론식 해석·다음 실행 시각 계산은 DoffKit 크론 탭.

try-catch · try-with-resources · 커스텀 예외

예외처리

// try - catch - finally
try {
    int n = Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.err.println("숫자 아님: " + e.getMessage());
} finally {
    // 예외와 무관하게 항상 실행 (정리 코드)
}

// try-with-resources — close() 자동 호출 (파일·DB 등 자원)
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
    String line = br.readLine();
} catch (IOException e) {
    e.printStackTrace();
}

// 예외 던지기
throw new IllegalArgumentException("id는 필수입니다");

JUnit 5 기본 골격 (@Test · assert · 예외 검증)

테스트 · JUnit 5

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator(); // 각 테스트 전에 새로 준비
    }

    @Test
    @DisplayName("두 수를 더한다")
    void add() {
        assertEquals(5, calc.add(2, 3)); // (기대값, 실제값) 순서
        assertTrue(calc.add(1, 1) > 0);
        assertNotNull(calc);
    }

    @Test
    @DisplayName("0으로 나누면 예외")
    void divideByZero() {
        IllegalArgumentException e = assertThrows(
            IllegalArgumentException.class,
            () -> calc.divide(10, 0));
        assertEquals("0으로 나눌 수 없습니다", e.getMessage());
    }
}

💡 assertEquals는 (기대값, 실제값) 순서 — 반대로 쓰면 실패 메시지가 헷갈린다. JUnit 5는 클래스·메서드가 public일 필요 없음. 테스트마다 인스턴스가 새로 만들어지므로 준비는 @BeforeEach로.

JUnit 4 ↔ 5 대응표 (레거시 마이그레이션)

테스트 · JUnit 4 ↔ 5

// JUnit 4 (폐쇄망 레거시)          →  JUnit 5
// ────────────────────────────────────────────────────
// import org.junit.Test;           →  org.junit.jupiter.api.Test
// @Before / @After                 →  @BeforeEach / @AfterEach
// @BeforeClass / @AfterClass       →  @BeforeAll / @AfterAll (static 유지)
// @Ignore                          →  @Disabled
// @RunWith(SpringRunner.class)     →  @ExtendWith(SpringExtension.class)
// @Test(expected = ...)            →  assertThrows(...)

// JUnit 4 방식 — 예외 "타입"만 검증 가능
@Test(expected = IllegalArgumentException.class)
public void oldStyle() {
    service.validate(null);
}

// JUnit 5 방식 — 예외 객체를 받아 메시지까지 검증
@Test
void newStyle() {
    IllegalArgumentException e = assertThrows(
        IllegalArgumentException.class, () -> service.validate(null));
    assertEquals("필수값 누락", e.getMessage());
}

💡 한 프로젝트에 4·5가 섞이면(junit-vintage 병행) @Test의 import 패키지부터 확인 — org.junit이면 4, org.junit.jupiter.api면 5. JUnit 4는 테스트 메서드가 public이어야 한다.

Mockito (@Mock · when/thenReturn · verify)

테스트 · Mockito

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    UserRepository repo;   // 가짜 객체

    @InjectMocks
    UserService service;   // 가짜를 주입받는 진짜 검증 대상

    @Test
    void findName() {
        // 스텁: repo.findById(1L)이 불리면 이 값을 돌려줘라
        when(repo.findById(1L)).thenReturn(new User(1L, "홍길동"));

        String name = service.findName(1L);

        assertEquals("홍길동", name);
        verify(repo).findById(1L);               // 정확히 1번 호출됐나
        verify(repo, never()).deleteById(any()); // 호출 안 됐나
    }
}

💡 when은 "불리면 이렇게 답하라"(스텁), verify는 "불렸는지 검사". any()·eq() 같은 매처를 하나라도 쓰면 그 호출의 나머지 인자도 전부 매처여야 한다(InvalidUseOfMatchersException 단골).

컨트롤러 테스트 (MockMvc — 서버 안 띄우고 API 검증)

테스트 · Spring Boot 3

@WebMvcTest(UserController.class) // 웹 계층만 띄운다 (서비스는 가짜로)
class UserControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    UserService service;

    @Test
    void getUser() throws Exception {
        when(service.find(1L)).thenReturn(new UserDto(1L, "홍길동"));

        mvc.perform(get("/api/users/1").accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.name").value("홍길동"));
    }

    @Test
    void createUser() throws Exception {
        mvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"홍길동\",\"age\":30}"))
           .andExpect(status().isCreated());
    }
}

💡 get()·status()·jsonPath()는 static import(MockMvcRequestBuilders·MockMvcResultMatchers)가 필요하다. Spring Boot 3.4+는 @MockBean이 @MockitoBean으로 이름이 바뀌었다(구버전 표기 유지 시 deprecated 경고).

Spring Data JPA (Repository · 파생 쿼리 · @Query)

DB 접근 · Spring Data JPA

import org.springframework.data.jpa.repository.*;

// 1) 인터페이스만 선언하면 구현은 Spring이 생성
public interface UserRepository extends JpaRepository<User, Long> {
    // 메서드 이름 규칙으로 SELECT 자동 생성
    List<User> findByCity(String city);
    Optional<User> findByEmail(String email);

    // 복잡하면 직접 JPQL
    @Query("select u from User u where u.age >= :age")
    List<User> findAdults(@Param("age") int age);
}

// 2) 사용 (서비스에서 주입받아)
User saved  = userRepository.save(user);                 // insert/update
User found  = userRepository.findById(1L).orElseThrow();
List<User> all = userRepository.findAll();
userRepository.deleteById(1L);

💡 findByXxx 이름 규칙으로 쿼리가 자동 생성된다. 컨트롤러(HTTP 서버 탭) → 서비스 → 이 Repository 순으로 이어진다.

순수 JDBC (PreparedStatement · ResultSet)

DB 접근 · JDBC

import java.sql.*;

String sql = "SELECT id, name FROM users WHERE age >= ?";

try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setInt(1, 20);                 // ? 바인딩 (인덱스는 1부터)

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            long id = rs.getLong("id");
            String name = rs.getString("name");
            System.out.println(id + " / " + name);
        }
    }
}
// INSERT/UPDATE/DELETE는 ps.executeUpdate() (영향 행 수 반환)

💡 값은 반드시 ? + setXxx로 바인딩(문자열 연결 금지 = SQL 인젝션). try-with-resources로 Connection·Statement가 자동 close된다.

MyBatis (매퍼 인터페이스 · 동적 쿼리)

DB 접근 · MyBatis

// 1) 매퍼 인터페이스 (애너테이션 방식)
@Mapper
public interface UserMapper {
    @Select("SELECT id, name FROM users WHERE id = #{id}")
    User findById(Long id);

    @Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")
    int insert(User user);
}

// 2) 동적 쿼리는 XML 매퍼가 유리
//   <select id="search" resultType="User">
//     SELECT * FROM users
//     <where>
//       <if test="city != null">AND city = #{city}</if>
//     </where>
//   </select>

// 3) 사용
User u = userMapper.findById(1L);

💡 #{...}는 PreparedStatement 바인딩(안전). #{} 대신 문자열 치환 구문을 쓰면 SQL 인젝션 위험이 있으니 컬럼·정렬 방향 등에만 제한적으로.

페이징 계산 (offset · 전체 페이지 · 블록 내비게이션)

DB 접근

// 입력: 전체 건수 · 현재 페이지(1부터 시작) · 페이지당 행 수
int totalCount = 237;
int page = 3;
int pageSize = 10;

// DB 조회용 offset — SELECT ... LIMIT pageSize OFFSET offset
int offset = (page - 1) * pageSize;

// 전체 페이지 수 — 올림 나눗셈 관용구
int totalPages = (totalCount + pageSize - 1) / pageSize; // 24

// 페이지 블록 내비게이션 (10개씩: 1~10 · 11~20 · 21~24)
int blockSize = 10;
int blockStart = ((page - 1) / blockSize) * blockSize + 1;
int blockEnd = Math.min(blockStart + blockSize - 1, totalPages);

boolean hasPrev = page > 1;
boolean hasNext = page < totalPages;

💡 페이지는 1부터, offset은 0부터 — 이 경계에서 한 페이지씩 밀리는 버그가 단골. totalCount가 0이면 totalPages도 0이므로 화면에선 최소 1로 보정. 대용량은 OFFSET 대신 키셋(WHERE id > 마지막 id).

파일 읽기 · 쓰기 (NIO Files · try-with-resources)

파일·저장 · JDK 8+ (java.nio)

Path path = Paths.get("C:/data/list.txt");

// 읽기 — 전체 (작은 파일. JDK 11+는 Files.readString도 가능)
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String all = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);

// 쓰기 — 덮어쓰기 / 이어쓰기
Files.write(path, "내용".getBytes(StandardCharsets.UTF_8));
Files.write(path, lines, StandardCharsets.UTF_8,
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

// 큰 파일 — 한 줄씩 스트리밍 (try-with-resources로 자동 닫기)
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// 존재 확인 · 폴더 생성
if (Files.notExists(path.getParent())) {
    Files.createDirectories(path.getParent());
}

💡 readAllLines는 파일 전체를 메모리에 올린다 — 대용량 로그는 newBufferedReader 스트리밍으로. 인코딩은 항상 명시 — 안 주면 OS 기본(한글 윈도우 MS949)에 끌려간다.

파일 업로드 받기 (MultipartFile — JS 업로드의 서버 짝)

파일·저장 · Spring Boot 3

@RestController
public class UploadController {

    // JS FormData의 append("file", ...)와 이름("file")이 맞아야 한다
    @PostMapping("/api/upload")
    public Map<String, Object> upload(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "memo", required = false) String memo) throws IOException {

        if (file.isEmpty()) {
            throw new IllegalArgumentException("파일이 비어 있습니다");
        }
        // 원본 파일명은 신뢰 금지(경로 조작 위험) — 저장명은 서버가 새로 만든다
        String saveName = UUID.randomUUID() + "_" + file.getOriginalFilename();
        Path target = Paths.get("C:/upload", saveName);
        Files.createDirectories(target.getParent());
        file.transferTo(target);

        Map<String, Object> res = new HashMap<>();
        res.put("name", saveName);
        res.put("size", file.getSize());
        return res;
    }
}

💡 @RequestParam 이름과 FormData 키가 다르면 400. 파일 크기 상한은 spring.servlet.multipart.max-file-size(기본 1MB) — 넘으면 컨트롤러 진입 전에 실패하므로 예외 처리도 바깥(@ControllerAdvice)에서.