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()(지수 표기 방지).
💡 타임아웃은 자리가 둘로 나뉜다 — 연결 대기는 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는 공백을 +로 바꾸므로 쿼리스트링 값 전용 — 경로(/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);
}
}
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() 등을 짧게 쓸 수 있다.
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로.
💡 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)에서.