
10월 14일부터 프리코스 첫 번째 과제가 출제되었다.
첫 주는 적응주 나는 적응중
다양한 개발을 경험 해보고, 여러 가지 활동을 해보면서 나는 좀 더 많은 사람들과 개발을 해보고싶다는 생각이 들었다. 특히 나보다 꼭 개발이 아니더라도 한 분야에 뛰어난 사람들과 개발이 해보고싶었다. 그래서 이번 우테코 8기에 지원하게 되었다.
이번 주 과제는 문자열 덧셈 계산기이다.
과제 해결에 들어가기 앞서, 먼저 알아야 할 것들이 있었다. 자바 코드 컨벤션, 커밋 컨벤션 등이 있다.
순서대로 봐보려고 한다.
자바 코드 컨벤션
우테코에서 적용하는 컨벤션은 우테코 방식을 곁들인(?) 구글의 자바 컨벤션이다. 꽤 긴 내용을 번역했다.(Gemini의 도움을 좀 받으면서 작성하였다 영알못...😂)
https://google.github.io/styleguide/javaguide.html
Google Java Style Guide
1 Introduction This document serves as the complete definition of Google's coding standards for source code in the Java™ Programming Language. A Java source file is described as being in Google Style if and only if it adheres to the rules herein. Like ot
google.github.io
이 문서에서, 별도의 명시가 없는 한 다음과 같이 용어를 사용
- class(클래스) 라는 용어는 일반 클래스, 레코드 클래스, 열거형 클래스(enum class), 인터페이스, 그리고 애너테이션 타입(@interface)을 모두 포함하는 포괄적인 의미로 사용
- member(멤버) 는 클래스의 중첩 클래스, 필드, 메서드, 생성자를 모두 포함하며, 즉 초기화 블록을 제외한 클래스의 모든 최상위 구성 요소를 의미
- comment(주석) 은 항상 구현 주석(implementation comments)을 가리킵니다. “documentation comments”라는 표현은 사용하지 않으며, 대신 일반적으로 쓰이는 용어인 Javadoc 을 사용
- 기타 “용어 참고사항”은 문서 전반에 걸쳐 필요할 때마다 추가로 제시
소스 파일 기본 사항
1. 파일 이름
클래스를 포함하는 소스 파일의 이름은 최상위 클래스(단 하나 존재함) 의 대소문자를 구분한 이름에 .java 확장자를 붙인 형태로 구성
2. 파일 인코딩: UTF-8
모든 소스 파일은 UTF-8 인코딩을 사용
3 특수 문자
3.1 공백 문자 (Whitespace characters)
줄바꿈 문자 시퀀스를 제외하면, 소스 파일 전체에서 사용되는 유일한 공백 문자는 ASCII 수평 공백 문자(0x20)
이 규칙은 다음을 의미한다:
- 그 외의 모든 공백 문자는 문자 리터럴(char), 문자열 리터럴(string), 텍스트 블록(text block) 내에서 이스케이프(escape) 처리되야함
- 탭(tab) 문자는 들여쓰기(indentation)에 사용하지 않음
3.2 특수 이스케이프 시퀀스 (Special escape sequences)
특수 이스케이프 시퀀스를 가진 문자(\b, \t, \n, \f, \r, \s, \", \', \\)는 해당 이스케이프 시퀀스를 사용해야 하며, 8진수(e.g. \012)나 유니코드(e.g. \u000a) 이스케이프는 사용하지 않음
3.3 Non-ASCII 문자 (Non-ASCII characters)
Non-ASCII 문자의 경우, 실제 유니코드 문자(e.g. ∞) 나 그에 상응하는 유니코드 이스케이프(e.g. \u221e) 를 사용할 수 있음
어느 쪽을 선택할지는 코드를 더 읽기 쉽고 이해하기 쉽게 만드는 쪽을 기준으로 함
단, 문자열 리터럴이나 주석 외부에서의 유니코드 이스케이프 사용은 지양함(변수명 같은걸로 사용하지 말아야 한다)
솔직히 이 부분에서 무슨 말인지 이해하기 어려웠다. 그래서 예제를 보고 이해했다.
int \u0061 = 5; // 이건 사실상 int a = 5; 와 같음! (컴파일러는 a로 인식)

소스파일 구조
일반적인 Java 소스 파일은 다음 순서로 구성:
- 라이선스 또는 저작권 정보 (있을 경우)
- package 선언
- import 문
- 하나의 최상위 클래스 선언
각 섹션 사이에는 정확히 한 줄의 빈 줄(blank line) 이 있어야 함
- package-info.java 파일은 클래스 선언을 제외하고 동일한 구조를 따름.
- module-info.java 파일은 package 선언이 없으며, 클래스 선언 대신 모듈 선언(module declaration) 을 포함하지만 그 외의 구조는 동일
1. 라이선스 또는 저작권 정보 (License or copyright information)
파일에 라이선스나 저작권 정보가 포함되어야 한다면, 반드시 파일의 맨 처음 부분에 작성
2. 패키지 선언 (Package declaration)
- package 선언은 줄바꿈(line-wrap) 하지 않음
- 열 제한(Section 4.4, Column limit: 100) 규칙은 package 선언에는 적용되지 않음
3. import 문 (Imports)
3.1 와일드카드 import 금지
* 를 사용하는 와일드카드(“on-demand”) import는 static import이든 일반 import이든 모두 금지
import java.util.*; // ❌ 사용 금지
3.2 줄바꿈 금지 (No line-wrapping)
- import 문은 줄바꿈하지 않음
- 열 제한(100자) 규칙은 import 문에도 적용되지 않음
3.3 순서 및 간격 (Ordering and spacing)
import 문은 다음과 같은 순서로 정렬:
- 모든 static import를 한 그룹으로 모음
- 모든 일반 import를 한 그룹으로 모음
- 두 그룹이 모두 있는 경우, 그 사이에 빈 줄 하나를 둠
- 같은 그룹 안에서는 ASCII 순서로 정렬
참고: “ASCII 순서로 정렬”은 import 문 전체가 아닌 import된 이름 기준 (.은 ;보다 먼저 오기 때문에 단순 문자열 정렬과 다를 수 있음)
3.4 정적 중첩 클래스(static nested class)에 대한 static import 금지
정적 중첩 클래스는 static import로 가져오지 않고, 일반 import를 사용
중첩 클래스란 아래와 같이 Outer 객체를 만들지 않고도 Outer.Nested로 바로 접근할 수 있는 클래스
// Outer.java
public class Outer {
public static class Nested {
public static void sayHello() {
System.out.println("Hello from Nested class!");
}
}
}
잘못된 사용 예제
import static Outer.Nested; // ❌ 이렇게 하면 안 됨
public class Example {
public static void main(String[] args) {
Nested.sayHello();
}
}
올바른 사용 예제
import Outer.Nested; // ✅ 일반 import로 가져오기
public class Example {
public static void main(String[] args) {
Nested.sayHello();
}
}
4 클래스 선언 (Class declaration)
4.1 하나의 최상위 클래스만 선언
각 최상위 클래스는 반드시 자신만의 소스 파일 안에 존재해야 함
즉, 하나의 .java 파일에는 하나의 public 클래스만 있어야 함
4.2 클래스 내부 구성 요소의 순서 (Ordering of class contents)
- 클래스의 멤버와 초기화 블록의 순서는 가독성과 이해도에 큰 영향을 미침
- 그러나 “정답”은 없으며, 클래스마다 논리적 구성이 다를 수 있음
- 중요한 점은, 누가 봐도 납득할 수 있는 논리적 순서(logical order) 로 구성되어야 함
예를 들어, 단순히 새 메서드를 “추가된 순서대로” 클래스의 맨 아래에 넣는 식은 좋지 않음
이 경우 “작성 순서(chronological order)”에 불과하며, 논리적이지 않음
4.2.1 오버로딩된 메서드는 분리하지 않음 (Overloads: never split)
- 같은 이름을 공유하는 메서드(또는 생성자)는 반드시 연속된 블록으로 함께 둠
- 그 사이에 다른 멤버(필드나 메서드 등)를 끼워 넣으면 안 됨
- 이 규칙은 접근 제어자(public, private)나 static 여부가 달라도 적용됨
5 모듈 선언 (Module declaration)
5.1 모듈 지시문(module directives)의 순서 및 간격
모듈 지시문은 다음 순서로 배치:
- requires 블록 : 모듈이 의존하는 다른 모듈을 지정
- exports 블록 : 다른 모듈이 접근할 수 있도록 공개할 패키지를 지정
- opens 블록 : 리플렉션(reflection)을 위해 공개할 패키지를 지정
- uses 블록 : 모듈이 사용할 서비스 인터페이스를 지정
- provides 블록 : 모듈이 구현하여 제공하는 서비스 구현 클래스를 지정
각 블록 사이에는 빈 줄 한 줄을 둠
module com.example.app {
// 1️⃣ 의존 모듈 선언
requires java.sql;
requires com.example.utils;
// 2️⃣ 외부에 공개할 패키지
exports com.example.app.api;
exports com.example.app.model;
// 3️⃣ 리플렉션을 허용할 패키지
opens com.example.app.internal;
// 4️⃣ 사용할 서비스 인터페이스
uses com.example.service.MyService;
// 5️⃣ 제공하는 서비스 구현체
provides com.example.service.MyService
with com.example.app.service.MyServiceImpl;
}
Formatting (서식 지정)
1.1 중괄호 사용 규칙 (Use of optional braces)
if, else, for, do, while 문에서는 항상 중괄호 {}를 사용
즉, 문법상 생략 가능하더라도 Google Style에서는 생략하지 않음
올바른 예제
if (condition) {
doSomething();
}
while (condition) {
}
잘못된 예제
if (condition)
doSomething();
코드를 추가하거나 유지보수 할 때 실수로 if/for에 코드가 잘못 포함되는 문제를 방지
1.2 비어 있지 않은 블록 (Nonempty blocks: K&R style)
K&R 스타일(Kernighan & Ritchie style)은 중괄호 배치 규칙 중 하나
Google Style은 이걸 따름
기본 규칙
- 여는 중괄호 { 앞에는 줄바꿈이 없음
- 여는 중괄호 { 뒤에는 줄바꿈 있음
- 닫는 중괄호 } 앞에는 줄바꿈 있음
- 닫는 중괄호 } 뒤에는 줄바꿈 있음,
- 단, 뒤에 else나 ,가 오면 줄바꿈 없음
올바른 예제 (K&R 스타일):
class Example {
void doSomething() {
if (condition) {
work();
} else {
rest();
}
}
}
잘못된 예제 (여는 중괄호 줄바꿈):
class Example
{
void doSomething()
{
if (condition)
{
work();
}
else
{
rest();
}
}
}
예외 (scope 제한용 임시 블록)
가끔 지역 변수의 범위를 제한하기 위해 중괄호로 임시 블록을 만드는 경우가 있음 이럴 때는 여는 중괄호 앞에 줄바꿈을 해도 됨
예시:
if (shouldDoSomething)
{
{
int temp = compute();
use(temp);
}
// temp는 여기서 더 이상 접근 불가
}
1.3 빈 블록 (Empty blocks: may be concise)
빈 블록은 두 가지 형태 모두 허용됩니다.
① K&R 스타일
void doNothing() {
}
② 간결한 한 줄 형태
void doNothing() {}
두 형태 모두 허용
예외: 여러 블록이 연결된 구문(if/else, try/catch/finally)
이런 경우에는 {} 한 줄로 쓰지 말고, K&R 스타일로 써야 합니다.
잘못된 예제:
try {
doSomething();
} catch (Exception e) {}
올바른 예제:
try {
doSomething();
} catch (Exception e) {
}
즉, 여러 블록이 연결되어 있을 때는 한눈에 블록 구조가 명확히 보이도록 {}를 따로 써야함
2. 블록 들여쓰기 (Block indentation: +2 spaces -> +4 spaces)
블록이 열릴 때마다 들여쓰기(indent)를 4칸 늘리고, 블록이 닫히면 다시 줄임
좋은 예제:
class Example {
void doSomething() {
if (condition) {
work();
} else {
rest();
}
}
}
- 클래스 본문: 들여쓰기 4칸
- 메서드 본문: 들여쓰기 4칸
- if/else 내부: 들여쓰기 4칸
- 즉, 블록이 중첩될 때마다 +4칸씩 추가
3 한 줄당 하나의 문장 (One statement per line)
각 문장은 항상 줄바꿈으로 구분 여러 문장을 한 줄에 쓰지 않음
좋은 예제:
int a = 1;
int b = 2;
return a + b;
나쁜 예제:
int a = 1; int b = 2; return a + b;
4 열(column) 제한: 100자
한 줄의 최대 길이는 100자를 넘지 않음
(100자를 초과할 경우 줄바꿈(line wrapping) 해야 함)
- “문자(character)”는 Unicode 기준 (즉, 한글·이모지 포함)
- package나 import 문에는 예외 적용 (길어도 허용)
좋은 예제:
String message = "This is a long text that still fits within the 100-character column limit.";
나쁜 예제 (줄이 너무 김):
String message = "This is a long text that goes far beyond the 100-character limit, which is not allowed in Google Java Style.";
너무 긴 줄은 Section 4.5에서 설명하는 줄바꿈 규칙(line-wrapping)을 적용
5 Line-wrapping (줄바꿈)
“Line-wrapping”이란, 원래 한 줄로 쓸 수 있는 코드를 여러 줄로 나누는 것을 의미
보통은 100자(column limit) 를 넘기지 않기 위해 줄바꿈을 하지만,
코드를 더 명확히 보이게 하려는 목적이라면 100자 이하라도 줄을 나눠도 됨
줄이 너무 길어질 때, 굳이 줄을 나누지 않고
→ 메서드나 지역 변수로 분리(extract) 하는 게 더 깔끔할 수도 있음
5.1 Where to break (줄을 끊는 위치)
원칙: “ 더 높은 구문 수준에서 줄을 끊어라 (prefer to break at a higher syntactic level)”
즉, 괄호나 연산자 단위 등 의미상 큰 단위에서 줄을 끊으라는 뜻
(1) 연산자(operator) 앞에서 줄을 끊는다 (대입 연산자 제외)
// 잘된 예
if (longExpression1
+ longExpression2
+ longExpression3 > threshold) {
...
}
+, &&, ||, > 같은 연산자 앞에서 끊기 -> 연산자의 왼쪽 항과 오른쪽 항이 더 쉽게 대응됨
(2) 대입 연산자(=)는 보통 뒤에서 줄바꿈
// 일반적인 스타일 (선호됨)
int sum =
a + b + c;
// 가능하지만 덜 일반적
int sum
= a + b + c;
(3) 메서드나 생성자 이름은 ( 와 함께 둔다
// 좋음
builder
.setName("Alice")
.setAge(30)
.setOccupation("Engineer")
.build();
// 나쁨 (이름과 괄호를 분리하면 가독성 저하)
builder.
setName("Alice");
(4) 쉼표(,)는 이전 토큰과 붙인다
// 좋음
callMethod(
param1, param2, param3,
param4);
(5) 람다(→)나 switch 화살표 앞뒤로 줄바꿈 금지
단, 화살표 뒤에 짧은 한 줄 표현식만 있는 경우에는 예외적으로 줄바꿈 가능
// 좋음
Predicate<String> predicate = str ->
longExpressionInvolving(str);
// 람다 본문이 블록일 때
MyLambda<String, Long, Object> lambda =
(String label, Long value, Object obj) -> {
...
};
// switch 화살표
switch (x) {
case ColorPoint(Color color, Point(int x, int y)) ->
handleColorPoint(color, x, y);
}
5.2 줄바꿈 시 들여쓰기 (Indent continuation lines at least +8 spaces)
줄바꿈을 하면, 다음 줄은 최소 +8칸 들여쓰기 해야 합니다.
(기본 블록 들여쓰기는 +4칸이지만, 줄바꿈은 그보다 더 깊게 들어감)
예시:
int result = someFunctionCall(param1, param2,
longExpression(param3, param4),
anotherMethod());
여러 줄이 같은 구조라면 같은 들여쓰기 사용
return longMethodName(param1, param2,
computeValue(param3, param4),
formatResult(param5, param6));
나쁜 예 (들여쓰기 불균형):
return longMethodName(param1, param2,
computeValue(param3, param4),
formatResult(param5, param6));
6 공백 (Whitespace)
6.1 세로 공백 (Vertical whitespace, 즉 빈 줄)
하나의 빈 줄(blank line) 은 다음 위치에 항상 사용:
- 클래스의 연속된 멤버나 초기화 블록 사이(즉, 필드, 생성자, 메서드, 중첩 클래스, static 초기화 블록, 인스턴스 초기화 블록 사이)
예외 1:
서로 연속된 필드(field) 사이에는 빈 줄이 선택적(optional)
필드들을 논리적으로 묶어서 구분할 필요가 있을 때만 빈 줄을 사용
예외 2:
enum 상수(enum constants) 사이의 빈 줄 규칙은 8.1절에서 다룸
또한, 다른 절에서 명시된 경우에도 (예: 3장: 소스 파일 구조(Section 3), 3.3절: Imports) 빈 줄이 필요할 수 있음
그 외에도, 코드의 가독성을 높이기 위해 다음과 같이 빈 줄을 사용할 수 있음:
- 논리적으로 구분되는 코드 블록 사이 (예: 여러 단계의 계산, 데이터 검증 후 처리 등)
클래스의 첫 번째 멤버 앞이나 마지막 멤버 뒤에 빈 줄을 두는 것은 권장도 비권장도 아님 (neither encouraged nor discouraged)
여러 개의 연속된 빈 줄도 허용되지만, 필요하지 않으면 사용하지 않는 것이 좋음(즉, 허용은 되지만 권장되지 않음)
6.2 가로 공백 (Horizontal whitespace, 즉 띄어쓰기)
언어 문법상 필수적인 경우나 다른 스타일 규칙에서 요구하는 경우를 제외하고,
리터럴(literal), 주석(comment), Javadoc 내부를 제외하면 ASCII 공백(space) 은 다음 위치에 한 칸만 들어감:
키워드와 괄호 사이
예:
if (condition) // O
if(condition) // X
else 또는 catch와 바로 앞의 } 사이
} else { // O
}catch { // X
여는 중괄호 { 앞, 단 두 가지 예외 있음:
- @SomeAnnotation({a, b}) → 공백 없음
- String[][] x = {{"foo"}}; → 중첩 중괄호 사이 공백 불필요
모든 이항/삼항 연산자 양쪽에 공백
예:
a + b
a == b ? x : y
이 규칙은 아래와 같은 “연산자 유사 기호(operator-like symbols)”에도 적용:
- 제네릭 타입 경계의 &: <T extends Foo & Bar>
- catch 블록의 여러 예외 구분 |: catch (FooException | BarException e)
- 향상된 for문의 콜론 :: for (int i : items)
- 람다의 화살표 ->: (x) -> x + 1
- switch의 rule 화살표: case "FOO" -> bar();
하지만 다음에는 적용되지 않음:
- 메서드 참조의 :: → Object::toString
- 점(.) 연산자 → object.toString()
쉼표(,), 콜론(:), 세미콜론(;), 또는 캐스팅 괄호의 닫는 괄호 ) 뒤
for (int i = 0; i < n; i++) {
System.out.println(i);
}
주석 시작 전후
- 코드와 // 사이에는 1칸 이상 공백
- //와 주석 내용 사이에도 1칸 이상 공백
예:
int x = 0; // loop counter
선언문에서 타입과 변수명 사이
List<String> list; // O
List<String>list; // X
배열 초기화 중괄호 내부의 선택적 공백
new int[] {5, 6} // O
new int[] { 5, 6 } // O
타입 애너테이션(type annotation) 과 [] 또는 ... 사이
@Nonnull String[] arr;
이 규칙은 줄의 처음이나 끝의 공백 여부를 의미하지 않음
즉, 라인 내부(interior space) 에서만 적용
6.3 가로 정렬(Horizontal alignment): 필수 아님
“가로 정렬(Horizontal alignment)” 이란, 코드의 특정 토큰(token, 예: 변수명이나 타입 등)을 윗줄의 토큰과 수평으로 맞추기 위해
여러 개의 공백(space)을 추가하는 관행을 말함
이 방식은 허용되지만, 절대 필수가 아님 또한 이미 가로 정렬된 코드가 있더라도, 그 정렬을 유지할 필요는 없음
예시:
// 정렬하지 않은 코드 — OK
private int x; // 괜찮음
private Color color; // 이것도 괜찮음
// 정렬한 코드 — 허용되지만, 유지할 필요 없음
private int x; // 허용됨, 하지만
private Color color; // 나중에 수정 시 정렬 깨질 수 있음
가로 정렬은 가독성을 높일 수도 있지만,
“정렬을 유지하기 위해서만” 줄을 수정하는 것은 오히려 문제를 만듬
예를 들어, 한 줄만 수정해야 하는 상황에서
정렬을 맞추려고 다른 줄의 공백까지 수정하면:
- 버전 히스토리가 더러워지고
- 코드 리뷰가 느려지며
- 머지 충돌(merge conflict)이 늘어남
즉, 정렬보다 협업 효율이 우선
7. 괄호 사용 (Grouping parentheses): 권장됨
괄호는 “필요 없다고 생각되어도” 가급적 사용하는 것을 권장
괄호를 생략해도 오해될 가능성이 전혀 없고,
괄호를 추가해도 가독성이 향상되지 않는다는 데 작성자와 리뷰어 모두가 동의하는 경우에만 생략
모든 개발자가 Java의 연산자 우선순위 표를 외우고 있지 않다는 점을 명심해야 함
즉, 괄호는 코드 명확성을 위해 적극적으로 사용하는 것이 좋음
8. 특정 구문(Specific constructs)
8.1 enum 클래스
enum 상수 뒤의 쉼표(,) 뒤에 줄바꿈(line break)을 넣을지 여부는 선택사항
또한 빈 줄(보통 한 줄 정도) 을 추가하는 것도 허용
예시:
private enum Answer {
YES {
@Override public String toString() {
return "yes";
}
},
NO,
MAYBE
}
만약 enum에 메서드나 상수에 대한 주석이 전혀 없을 경우,
다음처럼 배열 초기화 스타일(array initializer style) 로 한 줄로 작성할 수도 있음 (8.3.1절 참조).
private enum Suit { CLUBS, HEARTS, SPADES, DIAMONDS }
enum 도 결국 클래스이므로,
다른 클래스와 동일한 서식 규칙이 모두 적용
8.2 변수 선언 (Variable declarations)
8.2.1 한 선언당 하나의 변수 (One variable per declaration)
한 줄에 하나의 변수만 선언
다음과 같은 다중 선언은 사용하지 않음:
int a, b; // ❌ 금지
예외:
for 문 헤더에서의 다중 변수 선언은 허용됩니다.
for (int i = 0, j = n; i < j; i++, j--) { ... } // ✅ 허용
8.2.2 필요한 시점에 선언 (Declared when needed)
지역 변수(local variable)는 항상 사용 지점 근처에서 선언해야 함
즉, 블록(block)의 맨 위에서 습관적으로 변수를 몰아서 선언하지 않음
대신 처음 사용할 때 선언하고, 가능하면 즉시 초기화
예시:
// 권장 ❌: 블록 맨 위에 미리 선언
void process() {
int count;
...
count = items.size();
}
// 권장 ✅: 사용할 때 바로 선언
void process() {
int count = items.size();
}
이렇게 하면 변수의 스코프(scope) 가 최소화되고 코드의 가독성과 안정성이 향상
8.3 배열(Arrays)
8.3.1 배열 초기화(Array initializers): “블록처럼” 작성 가능
배열 초기화 구문({} 안의 값 나열)은 “블록(block) 구조처럼” 들여쓰기하여 작성할 수 있음
즉, 한 줄로 쓰거나 여러 줄로 나누어도 모두 허용됨
다음 예시 모두 유효한 코드 (단, 전체 가능한 형태는 아님):
new int[] { 0, 1, 2, 3 }
new int[] {
0, 1, 2, 3
}
new int[] {
0,
1,
2,
3,
}
new int[]
{0, 1, 2, 3}
즉, 배열 초기화 시 들여쓰기 방식은 자유롭지만 일관성 있게 유지하는 것이 중요
8.3.2 C 스타일 배열 선언 금지
C 스타일 배열 선언(String args[])은 사용하지 않음
대괄호([])는 변수 이름이 아니라 타입(type) 의 일부로 봄
올바른 방식:
String[] args;
잘못된 방식:
String args[];
8.4 switch 문과 switch 표현식 (Switch statements and expressions)
Java에는 역사적으로 두 가지 switch 문법이 존재:
- 옛 스타일(old-style) — : 사용
- 새 스타일(new-style) — -> 사용 (Java 14 이후)
용어 정리 (Terminology Note)
- Switch 블록 내부에는 다음 중 하나가 들어감:
- 하나 이상의 switch rule (새 스타일)
- 하나 이상의 statement group (옛 스타일)
새 스타일 (new-style)
- case 또는 default 뒤에 -> 가 오며, 뒤에는 표현식(expression), 블록(block), 또는 throw 문이 옴
예:
case 1 -> System.out.println("one");
옛 스타일 (old-style)
- case 또는 default 뒤에 : 가 오며, 그 아래에 실행할 문(statement) 들이 배치
예:
case 1:
System.out.println("one");
break;
8.4.1 들여쓰기 (Indentation)
switch 블록 내부의 내용은 +2칸 들여쓰기 함
모든 case 라벨(case, default)은 이 들여쓰기 수준에서 시작
새 스타일 switch 예시:
switch (number) {
case 0, 1 -> handleZeroOrOne();
case 2 ->
handleTwoWithAnExtremelyLongMethodCallThatWouldNotFitOnTheSameLine();
default -> {
logger.atInfo().log("Surprising number %s", number);
handleSurprisingNumber(number);
}
}
규칙 정리:
- -> 뒤에 한 줄로 표현 가능하면 한 줄에 둬도 됨
- 단, 중괄호 {} 블록이 있다면 { 뒤에는 반드시 줄바꿈 해야 함
- 블록 내부({ ... })는 한 단계(+2) 더 들여쓰기
옛 스타일 switch 예시:
switch (number) {
case 0:
handleZero();
break;
case 1:
handleOne();
break;
default:
handleDefault();
}
옛 스타일에서는 case 뒤의 : 다음에 줄바꿈이 반드시 옴
각 문(statement)은 추가로 +2칸 들여쓰기
8.4.2 Fall-through 주석
옛 스타일 switch에서, 하나의 case가 break 없이 다음 case로 “흘러가는” 경우, 그 의도를 명시적으로 주석으로 표시
이 주석은 // fall through 형태로 자주 쓰임
switch (input) {
case 1:
case 2:
prepareOneOrTwo();
// fall through ← 이 부분 중요!
case 3:
handleOneTwoOrThree();
break;
default:
handleLargeNumber(input);
}
규칙 요약:
- “fall-through” 주석은 의도적인 흐름임을 표시하는 것
- 마지막 case 뒤에는 필요 없음
- 새 스타일(->)에서는 fall-through 불가능, 따라서 주석도 불필요
8.4.3 switch의 완전성(Exhaustiveness)과 default 라벨
모든 switch문은 “완전해야(exhaustive)” 함
즉, 모든 가능한 값이 반드시 하나의 case에 매칭되어야 함
이는 다음 두 경우로 충족될 수 있음:
- default 라벨이 존재할 때
- enum의 모든 상수를 명시했을 때
Google Style에서는 언어 차원에서 필수가 아니더라도, 모든 switch문이 완전해야 한다고 요구
예를 들어, default가 실제 코드가 없어도 아래처럼 작성:
switch (status) {
case SUCCESS -> handleSuccess();
case ERROR -> handleError();
default -> {} // 코드가 없어도 OK — 완전성 보장
}
8.4.4 switch 표현식 (Switch expressions)
switch 표현식은 반드시 새 스타일(new-style) 문법을 사용
(즉, -> 기반 구조)
예시:
return switch (list.size()) {
case 0 -> "";
case 1 -> list.getFirst();
default -> String.join(", ", list);
};
특징:
- switch 전체가 값을 반환(return) 하는 표현식으로 사용됨
- 각 case는 반드시 ->로 값을 돌려주거나 예외를 던짐
- break 문이 필요 없음
8.5 어노테이션 (Annotations)
8.5.1 타입 사용 어노테이션 (Type-use annotations)
타입 사용 어노테이션은 주석이 붙는 타입 바로 앞에 위치
@Target(ElementType.TYPE_USE)로 메타 어노테이션된 경우, 그 어노테이션은 타입 사용 어노테이션
예시:
final @Nullable String name;
public @Nullable Person getPersonByName(String name);
8.5.2 클래스, 패키지, 모듈 어노테이션 (Class, package, and module annotations)
클래스, 패키지, 모듈 선언에 적용되는 어노테이션은 Javadoc 블록 바로 뒤에 작성하며, 각 어노테이션은 한 줄에 하나씩 적음
줄바꿈은 “라인 래핑(line-wrapping)”이 아니므로 들여쓰기를 추가하지 않음
예시:
/** This is a class. */
@Deprecated
@CheckReturnValue
public final class Frozzler { ... }
/** This is a package. */
@Deprecated
@CheckReturnValue
package com.example.frozzler;
/** This is a module. */
@Deprecated
@SuppressWarnings("CheckReturnValue")
module com.example.frozzler { ... }
8.5.3 메서드 및 생성자 어노테이션 (Method and constructor annotations)
메서드와 생성자의 어노테이션 규칙은 위(8.5.2)와 동일
예시:
@Deprecated
@Override
public String getNameIfPresent() { ... }
단, 매개변수가 없는(single, parameterless) 어노테이션은 메서드 선언 첫 줄에 함께 쓸 수도 있음
예시:
@Override public int hashCode() { ... }
8.5.4 필드 어노테이션 (Field annotations)
필드 어노테이션은 Javadoc 블록 바로 뒤에 작성하며, 여러 개의 어노테이션을 같은 줄에 나란히 쓸 수도 있음
예시:
/**
* 데이터 로더 객체입니다.
*/
@Partial @Mock DataLoader loader;
8.5.5 매개변수 및 지역 변수 어노테이션
매개변수나 지역 변수에 대한 어노테이션은 별도의 형식 규칙이 없음
단, 타입 사용 어노테이션(@Target(TYPE_USE))은 예외로 위 규칙(8.5.1)을 따름
8.6 주석 (Comments)
구현 주석(implementation comments)에 관한 내용(Javadoc은 Section 7에서 따로 다룸)
빈 줄처럼 보이더라도, 그 줄에 주석이 포함되어 있으면 실제로는 비어 있지 않은 줄로 간주
8.6.1 블록 주석 스타일 (Block comment style)
블록 주석은 주변 코드와 같은 들여쓰기 레벨에 맞춤
/* ... */ 또는 // ... 스타일 모두 허용
예시:
/*
* This is // And so /* Or you can
* okay. // is this. * even do this. */
*/
주석을 별표(****)나 기타 문자로 상자(box) 형태로 감싸지 않음
여러 줄 주석을 쓸 때, 자동 코드 포매터가 줄을 다시 맞춰주길 원한다면 /* ... */ 스타일을 사용
대부분의 포매터는 // ... 스타일은 자동 줄맞춤을 하지 않음
8.6.2 TODO 주석
임시 코드, 단기 해결책, 또는 개선이 필요한 부분에 TODO 주석을 사용
형식은 다음과 같음
// TODO: [참조 링크] - [설명]
가장 좋은 TODO는 버그 트래킹 시스템 링크를 포함 이유는 추적과 후속 논의가 가능하기 때문
예시:
// TODO: crbug.com/12345678 - Remove this after the 2047q4 compatibility window expires.
다음과 같은 개인 이름 기반의 TODO는 피하세요:
// TODO: @yourusername - File an issue and use a '*' for repetition.
“나중에 수정할 것” 형식의 TODO는 명확한 날짜나 조건을 반드시 포함
예:
// TODO: Fix by November 2005
// TODO: Remove this code when all clients can handle XML responses.
8.7 수정자 (Modifiers)
클래스와 멤버 선언의 수정자(modifier)는 Java Language Specification이 권장하는 순서로 작성
public protected private abstract default static final sealed non-sealed
transient volatile synchronized native strictfp
모듈 선언(requires 지시문)의 수정자는 다음 순서를 따름:
transitive static
8.8 숫자 리터럴 (Numeric Literals)
long 타입의 정수 리터럴은 소문자 l(l) 대신 대문자 L을 사용해야 함
이유: 숫자 1과 혼동될 수 있기 때문
올바른 예시:
long population = 3000000000L; // 올바른 표기
잘못된 예시:
long population = 3000000000l; // 소문자 l은 1과 헷갈림
8.9 텍스트 블록 (Text Blocks)
여러 줄 문자열(""")을 사용할 때의 포맷 규칙
- 여는 따옴표(""")는 새 줄에서 시작해야 함
- 닫는 따옴표(""")는 여는 따옴표와 동일한 들여쓰기 위치에 있어야 함
- 닫는 따옴표 뒤에는 코드가 바로 이어질 수도 있음
- 블록 안의 각 줄은 여는/닫는 따옴표보다 적어도 같은 수준으로 들여쓰기해야 함
- 텍스트 블록의 내용은 열(column) 제한(100자)을 초과해도 괜찮음
예시:
String message = """
Hello, world!
This is a text block.
""";
또는 아래처럼 가능
String json = """
{
"name": "Juntae",
"age": 25
}
""";
이름 규칙 (Naming)
1. 모든 식별자(Identifier)에 공통적인 규칙
- 영문자(A–Z, a–z), 숫자(0–9), 그리고 **언더스코어(_)**만 사용.
- 따라서 정규식으로 표현하면:
\w+
- 접두사나 접미사를 붙이지 않음(즉, 아래와 같은 이름은 Google Style이 아님)
잘못된 예시:
name_, mName, s_name, kName
올바른 예시:
name, userName, songTitle
2. 식별자 유형별 규칙
2.1 패키지와 모듈 이름
- 소문자와 숫자만 사용, 언더스코어(_) 금지.
- 단어는 그냥 이어 붙이기(concatenation).
- 예시:
com.example.deepspace ✅
com.example.deepSpace ❌
com.example.deep_space ❌
2.2 클래스 이름
- UpperCamelCase 사용 (단어의 첫 글자를 대문자로)
- 예: Character, ImmutableList, HashTable
- 명사나 명사구 형태로 작성하는 것이 일반적
- (예: User, MusicPlayer, DataLoader)
- 인터페이스 이름은 명사/형용사 둘 다 가능
- (예: Readable, List)
- 테스트 클래스는 반드시 Test로 끝남
- 예: HashIntegrationTest, UserServiceTest
2.3 메서드 이름
- lowerCamelCase 사용 (첫 단어는 소문자, 이후 단어는 대문자로 시작)
- 예: sendMessage, getUserInfo, stop
- 동사나 동사구 형태로 작성 → 메서드는 “행동”을 나타내기 때문
- JUnit 테스트 메서드에서는 언더스코어 _ 사용 허용됨
- 테스트 목적을 더 명확히 표현할 수 있도록
예시:
@Test
void transferMoney_deductsFromSource() { ... }
"transferMoney" (행동) + "deductsFromSource" (기대 결과)를 구분하기 좋음
2.4 상수 이름 (Constant names)
상수(constant)의 이름은 UPPER_SNAKE_CASE 형식을 사용
모든 문자는 대문자이며, 단어 사이는 밑줄(_) 로 구분
상수는 static final 로 선언된 필드 중에서, 그 내용이 완전히 불변(immutable) 이고, 해당 객체의 메서드가 부작용(side effect) 을 일으키지 않는 것들을 말함
예를 들어:
- 기본형(primitive type)
- 문자열(String)
- 불변 객체(Immutable value class)
- null 값
등이 이에 해당
객체의 상태가 변경될 가능성이 있다면, 그것은 상수가 아님
단지 “변경하지 않겠다”고 의도만 하는 것은 상수로 인정되지 않음
상수 예시:
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final Map<String, Integer> AGES = ImmutableMap.of("Ed", 35, "Ann", 32);
static final Joiner COMMA_JOINER = Joiner.on(','); // Joiner는 불변 객체
static final SomeMutableType[] EMPTY_ARRAY = {};
상수가 아닌 예시:
static String nonFinal = "non-final"; // final 아님
final String nonStatic = "non-static"; // static 아님
static final Set<String> mutableCollection = new HashSet<>(); // 변경 가능
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable); // 내부 요소 변경 가능
static final Logger logger = Logger.getLogger(MyClass.getName()); // 상태 변경됨
static final String[] nonEmptyArray = {"these", "can", "change"}; // 배열 요소 변경 가능
이런 이름들은 보통 명사 혹은 명사구(noun phrase) 로 구성
2.5 비상수 필드 이름 (Non-constant field names)
상수가 아닌 필드(static 여부와 관계없이)는 lowerCamelCase 를 사용
이 이름들도 보통 명사나 명사구로 지으며, 예를 들어 computedValues, index 와 같이 씀
2.6 매개변수 이름 (Parameter names)
매개변수 이름은 lowerCamelCase 를 사용
공개(public) 메서드에서는 한 글자짜리 이름(예: x, y, n)은 피해야 함
2.7 지역 변수 이름 (Local variable names)
지역 변수 이름도 lowerCamelCase 를 사용
비록 final 이고 불변하더라도, 지역 변수는 상수가 아니므로 UPPER_SNAKE_CASE 를 쓰지 않음
2.8 타입 변수 이름 (Type variable names)
타입 변수(제네릭에서 사용하는 T, E 등)는 두 가지 스타일 중 하나로 작성:
- 한 글자 대문자, 필요시 숫자를 붙일 수 있음 → 예: E, T, X, T2
- 클래스 이름 형식 + T → 예: RequestT, FooBarT
3. 카멜 케이스 정의 (Camel case: defined)
때때로 “카멜 케이스(camel case)”로 영어 문구를 바꾸는 데 여러 가지 합리적인 방법이 있음
예를 들어 “IPv6”, “iOS” 같은 특이한 단어들 때문
예측 가능성을 높이기 위해 Google Style은 다음의 거의 결정적인 규칙(deterministic scheme) 을 정의
변환 단계:
- 문장(영문 구절)을 ASCII로 변환하고, 아포스트로피(’) 를 제거
- 예: "Müller's algorithm" → "Muellers algorithm"
- 결과를 공백 및 구두점(주로 하이픈) 기준으로 단어별로 나눔
- (선택 사항) 이미 카멜케이스 형태로 널리 쓰이는 단어라면,예: "AdWords" → "ad words"
- 하지만 "iOS" 같은 단어는 예외 (정해진 규칙 없음).
- 이를 개별 단어로 분리합니다.
- 모든 단어를 소문자로 바꾸고,
- UpperCamelCase → 각 단어의 첫 글자 대문자
- lowerCamelCase → 첫 단어는 소문자, 나머지는 대문자 시작
- 다음 중 하나의 규칙으로 대문자화:
- 단어들을 모두 붙여서 하나의 식별자(identifier) 로 생성
- 원래 단어의 대소문자는 거의 무시
주의: 숫자는 대소문자가 없으므로, 숫자가 연속될 경우_(언더스코어)를 써서 구분할 수도 있습니다. 예: guava33_4_6
올바른 예시:
문장(Prose form), 올바른 이름, 잘못된 이름
| “XML HTTP request” | XmlHttpRequest | XMLHTTPRequest |
| “new customer ID” | newCustomerId | newCustomerID |
| “inner stopwatch” | innerStopwatch | innerStopWatch |
| “supports IPv6 on iOS?” | supportsIpv6OnIos | supportsIPv6OnIOS |
| “YouTube importer” | YouTubeImporter | YoutubeImporter (허용되지만 비추천) |
| “Turn on 2SV” | turnOn2sv | turnOn2Sv |
| “Guava 33.4.6” | guava33_4_6 | guava3346 |
참고: 영어에서는 “nonempty”와 “non-empty” 모두 올바른 표현이므로 checkNonempty와 checkNonEmpty 둘 다 허용
프로그래밍 관행
1. @Override: 항상 사용
@Override 애너테이션은 사용 가능한 모든 경우에 반드시 표시
이는 다음과 같은 경우를 포함:
- 클래스 메서드가 슈퍼클래스의 메서드를 오버라이드할 때
- 클래스 메서드가 인터페이스의 메서드를 구현할 때
- 인터페이스 메서드가 상위 인터페이스의 메서드를 재정의(respecify) 할 때
- 레코드(record) 구성 요소에 대한 명시적으로 선언된 접근자 메서드(accessor method) 일 때
예외:
부모 메서드가 @Deprecated로 표시된 경우에는 @Override를 생략
2. 예외 처리: 무시하지 않음
catch 블록에서 아무 작업도 하지 않는 것은 거의 항상 잘못된 방식
일반적인 대응 방법은 다음과 같다:
- 예외를 로그(log) 남기기
- 예외가 “발생할 수 없는 상황”이라면 AssertionError로 다시 던지기(rethrow)
만약 정말로 catch 블록에서 아무 작업도 하지 않는 것이 적절한 경우, 그 이유를 반드시 주석으로 명시
try {
int i = Integer.parseInt(response);
return handleNumericResponse(i);
} catch (NumberFormatException ok) {
// 숫자가 아닌 경우, 괜찮음. 계속 진행.
}
return handleTextResponse(response);
3. 정적(static) 멤버: 클래스 이름으로 한정
정적 클래스 멤버에 접근할 때는 그 클래스의 이름으로 접근해야 하며, 해당 클래스 타입의 참조 변수나 표현식으로 접근하면 안 됨
Foo aFoo = ...;
Foo.aStaticMethod(); // ✅ 좋음
aFoo.aStaticMethod(); // ❌ 나쁨
somethingThatYieldsAFoo().aStaticMethod(); // 🚫 매우 나쁨
4. 파이널라이저(finalizer): 사용 금지
Object.finalize 메서드를 오버라이드하지 않음
자바의 파이널라이제이션(finalization) 기능은 제거 예정(deprecated)
Javadoc (자바독)
1. 형식(Formatting)
1.1 기본 형태(General form)
Javadoc 블록의 기본 형식은 다음 예시와 같다:
/**
* 여러 줄의 Javadoc 텍스트를 여기에 작성하며,
* 일반적인 줄 바꿈 규칙을 따릅니다...
*/
public int method(String p1) { ... }
또는 짧은 한 줄짜리 예시도 가능:
/** 아주 짧은 Javadoc 예시입니다. */
기본적인 여러 줄 형식은 언제나 허용
단, Javadoc 전체가 한 줄 안에 완전히 들어갈 경우에는 한 줄짜리 형식을 대신 사용
단, @param 같은 블록 태그(block tag) 가 포함된 경우에는 한 줄짜리 형식을 사용하면 안 됨
1.2 문단(Paragraphs)
문단 사이, 그리고 블록 태그(@param 등) 앞에는 빈 줄 하나를 넣음(이때 빈 줄은 선행 별표 *만 포함된 줄을 의미)
각 문단은 다음 규칙을 따름:
- 첫 번째 문단을 제외한 모든 문단은 <p>로 시작하며, <p> 뒤에는 공백이 없음
/**
* Calculates the average score of a student based on their exam results.
*
* <p>This method takes a list of integer scores and returns the average value as a double.
* If the list is empty, it returns 0.0.
*
* @param scores the list of exam scores to calculate the average from
* @return the average score, or 0.0 if the list is empty
* @throws IllegalArgumentException if any score is negative
* @deprecated Use {@link #calculateAverage(Collection)} instead.
*/
public double calculateAverage(List<Integer> scores) { ... }
- <ul>, <table> 등의 HTML 블록 요소는 <p>로 시작하지 않음
/**
* Processes student records and performs the following operations:
*
* <ul>
* <li>Validates the student data
* <li>Calculates GPA
* <li>Updates the database
* </ul>
*
* @param student the student object containing academic records
* @return true if the record was successfully processed
* @throws DatabaseException if a database access error occurs
*/
public boolean processStudentRecord(Student student) { ... }
1.3 블록 태그(Block tags)
표준 블록 태그는 아래 순서로 작성:
@param, @return, @throws, @deprecated
이 네 가지 태그는 항상 설명이 함께 있어야 하며, 빈 설명으로 작성해서는 안 됨
또한, 태그 설명이 한 줄에 다 들어가지 않을 경우, 다음 줄은 @ 위치로부터 4칸 이상 들여쓰기
2. 요약 구문(The summary fragment)
모든 Javadoc 블록은 간단한 요약 문장(summary fragment) 으로 시작
이 부분은 클래스나 메서드 목록 등 특정 문맥에서 유일하게 표시되는 부분이므로 매우 중요
요약 문장은 다음과 같은 특징을 가짐:
- 명사구(noun phrase) 또는 동사구(verb phrase) 로 작성
- 완전한 문장 형태가 아님
- 예를 들어, 아래와 같은 문장은 잘못된 형태:
- A {@code Foo} is a...
- This method returns...
- Save the record. (명령문)
하지만 대문자로 시작하고, 마침표로 끝나는 등 문장처럼 보이도록 작성
잘못된 예:
/** @return the customer ID */
올바른 예:
/** Returns the customer ID. */
or
/** {@return the customer ID} */
3 Javadoc 사용 위치(Where Javadoc is used)
최소한 Javadoc은 모든 공개(visible) 클래스, 멤버, 또는 레코드 구성 요소에 존재
가시성(visibility) 규칙:
- 최상위 클래스(top-level class): public일 경우 가시적
- 멤버(member): public 또는 protected이면서, 포함된 클래스가 가시적일 경우
- 레코드 구성 요소(record component): 포함된 레코드가 가시적일 경우
추가적인 Javadoc은 필요에 따라 작성(자세한 내용은 3.4 “필수 아님 Javadoc” 참고)
3.1 예외: 자명한 멤버(Self-explanatory members)
“단순하고 자명한” 멤버나 레코드 구성 요소의 경우, 예를 들어 getFoo()처럼 설명할 내용이 전혀 없는 경우, Javadoc은 선택 사항(optional)
단, 독자가 혼란스러울 수 있는 경우에는 이 예외를 적용해서는 안 됨.
예를 들어 canonicalName 이라는 구성 요소가 있을 때, 독자가 “canonical name”이 무엇인지 모를 가능성이 있다면 반드시 Javadoc을 작성
3.2 예외: 오버라이드된 메서드(Overrides)
상위 타입의 메서드를 오버라이드(override) 하는 경우, 항상 Javadoc을 작성할 필요는 없음
3.4 필수 아님 Javadoc(Non-required Javadoc)
다른 클래스, 멤버, 레코드 구성 요소의 경우, 필요하거나 원하는 경우에만 Javadoc을 작성
클래스나 멤버의 전체 목적 또는 동작을 설명하기 위한 주석이 있다면,
그 주석은 일반 주석(// 또는 /* */) 대신 Javadoc (/**) 으로 작성
이 경우에는 형식(1.1~1.3, 2) 의 규칙을 반드시 따를 필요는 없지만, 가능하면 따르는 것을 권장
지금까지 자바 코드 컨벤션을 살펴보았다. 다음은 커밋 컨벤션을 살펴보자.
커밋 컨벤션
<유형>(<범위>): <제목>
<본문>
<꼬리말>
제목은 유형, 범위, 제목 세 부분으로 구성
- 유형 (Type): 커밋의 종류를 나타냄
- feat: 새로운 기능 추가
- fix: 버그 수정
- docs: 문서 변경
- style: 코드 스타일 변경 (포맷, 세미콜론 등)
- refactor: 코드 리팩토링
- test: 테스트 코드 추가 또는 수정
- chore: 빌드 프로세스, 패키지 매니저 설정 등 유지보수 작업
- 범위 (Scope): 커밋이 영향을 미치는 코드의 위치나 부분을 명시 (예: feat(parser): ...)
- 제목 (Subject): 변경 사항에 대한 간결한 설명
- 현재 시제를 사용 (예: "change" O, "changed" X)
- 첫 글자를 대문자로 쓰지 않음
- 문장 끝에 마침표를 찍지 않음
본문
본문은 선택 사항이며, 제목만으로 설명이 부족할 때 사용
- 무엇을, 왜 변경했는지 상세히 설명
- 이전 동작과 어떻게 달라졌는지 설명
꼬리말
꼬리말도 선택 사항이며, 특정 키워드를 사용하여 추가 정보를 제공
- 주요 변경 사항 (Breaking Changes): 이전 버전과 호환되지 않는 큰 변경 사항이 있을 경우 BREAKING CHANGE: 로 시작하여 설명
- 이슈 참조 (Referencing issues): 관련된 이슈가 있다면 Closes #123 와 같이 이슈 번호를 기재
이제 커밋 컨벤션까지 살펴보았다. 이제 본격적으로 1주차 과제를 살펴보겠다.
문자열 덧셈 계산기
과제의 설명에는 이렇게 적혀 있었다. 우선 먼저, 기능 요구사항을 정리해서 readme에 구현 할 순서대로 정리해서 써봐라
"구현할 기능 목록을 정리" 이 말을 보고, 어떻게 기능 목록을 구분해서 분리해야 할까? 고민했었다.
결론은 "객체지향적 설계를 위해서 역할별로 클래스 분리를 하자."였다.
그래서 기능이 무엇이 있는지부터 생각해봤다. 우선 수행되어야 할 기능은 구분자를 파악하는 것이다.
기본적으로 주어지는 구분자인 "," 와 ":" 그리고 추가적으로 사용자 지정 구분자 여기서 핵심은 사용자 지정 구분자를 어떻게 Parse 할 것인가 였다.
주어진 조건은 "//" 다음에 오고 "\n" 이전에 오는 문자를 사용자 지정 구분자라고 지정하는 것이었다.
그래서 덧셈 연산을 하는 Calculator와 구분자를 Parsing하는 DelimiterParser로 클래스를 나누었다.
우선 DelimiterParser의 기능 요구사항을 readme에 작성해보자.
DelimiterParser
DelimiterParser
1. 쉼표(,) 또는 콜론(:)을 구분자로 입력
2. " // "와 " \n " 사이에 입력받는 문자를 구분자로 지정
3. 구분자들은 혼합해서 쓸 수 있음 ex) 1,2:3
예외 처리
1. 구분자가 연속으로 두번 입력되는 경우 값이 없음으로 parser에서 삭제 ex) 1::2,3 -> 1, ' ', 2, 3 중에 ' '는 삭제
2. 사용자 지정 구분자가 여러개 혹은 중간에 추가
3. 구분자를 기준으로 parse 할 때 숫자가 맞는지 확인(양수인지 확인)
위 내용처럼 기능 요구사항과 예외 처리를 작성했다
예외 입력으로 아래처럼 들어오는 경우도 고려하여 다중 구분자 사용 예외 처리를 진행했다
//b\n3,4,5b8b9//c\n10c11
그럼 실제 코드를 살펴보자
package calculator.domain;
import calculator.common.NumberValidator;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 문자열에서 구분자를 파싱하고 숫자 목록을 추출하는 클래스. 기본 구분자(','와 ':')와 커스텀 구분자를 지원합니다.
*/
public class DelimiterParser {
private static final Pattern CUSTOM_DELIMITER_PATTERN = Pattern.compile("//(.*?)\\\\n");
private final Set<String> delimiters;
public DelimiterParser() {
delimiters = new HashSet<>(List.of(",", ":"));
}
/**
* 입력 문자열을 파싱하여 숫자 목록을 반환합니다.
*
* @param input 파싱할 전체 문자열
* @return 파싱된 숫자(Integer)의 리스트
*/
public List<Integer> parse(String input) {
addCustomDelimiter(input);
String numbers = removeDelimiterDeclarations(input);
String regex = String.join("|", delimiters);
String[] tokens = numbers.split(regex);
NumberValidator.validateNumbers(tokens);
return Arrays.stream(tokens)
.filter(s -> s != null && !s.isEmpty())
.map(Integer::parseInt)
.toList();
}
private void addCustomDelimiter(String input) {
Matcher matcher = CUSTOM_DELIMITER_PATTERN.matcher(input);
while (matcher.find()) {
delimiters.add(Pattern.quote(matcher.group(1)));
}
}
private String removeDelimiterDeclarations(String input) {
return input.replaceAll(CUSTOM_DELIMITER_PATTERN.pattern(), ",");
}
}
DelimiterParser를 구현하면서 중요했던 부분은 Pattern 부분이었다. 자바 문자열 안에서는 "\"를 "\\"처럼 두 번 써야 실제 정규식에서 "\"로 인식된다는 것이다.
내부 동작 단계는 아래처럼 4단계로 이루어진다.
- 컴파일 단계 (Pattern.compile)
- 정규식 문법 분석
- 내부 상태 머신(NFA/DFA) 생성
- 매칭 객체 생성 (pattern.matcher)
- 문자열과 패턴을 연결
- 매칭 진행을 위한 상태 초기화
- 매칭 단계 (matcher.find, matcher.matches, matcher.replaceAll 등)
- 문자열을 순회하며 패턴과 비교
- 그룹, 반복, 선택(Alternation) 등을 처리
- 결과 반환
- 매칭된 부분, 그룹, 시작/끝 인덱스 등 제공
"\n"을 인식시키기 위해서는 "\\\\n"으로 작성 해야합니다. 처음에는 이게 뭐야??라는 생각이 들었고, 이해하기 힘들었다...
그래서 두 번의 번역이 이루어진다는걸 생각해서 쪼개서 생각해보았다. "\\", "\\n"이렇게 두 개로 나누었을 때, 첫번째 번역에서는
"\\" -> "\", "\\n" -> "\n"이고, 그러면 결과는 "\\n"으로 됩니다. 그러면 이제 다음 번역으로 넘어가면 "\n"라는 문자 자체로 인식이 되는 것이다.
식으로 정리해보면 아래와 같다.
"\\" + "\\n" -> "\" + "\n" = "\\n"
"\\n" -> "\n"
이를 통해서 정규표현식에서 이스케이프 문자에 대한 자바 문자열 안에서의 번역 방법을 이해했다.
자주 사용하지 않던 정규표현식을 공부할 수 있는 기회였던 것 같다.
추가로 아래와 같은식으로 표현 할 수도 있다.
Pattern.compile("//(.*?)" + Pattern.quote("\\n"));
quote는 메타문자를 일반 문자로 인식하게 하는 메서드이다. ex) . -> \.으로 인식 시킴
이제 이 Pattern을 사용해서 구분자를 찾아내고, 분리해내는 과정을 위에 작성한 코드처럼, 사용자 지정 구분자를 찾아서 구분자 Set에 추가하는 addCustomDelimiter()와 Set에 추가된 구분자를 문자열에서 지우는 removeDelimiterDeclarations()를 구현하여 parse()에서 처리해주었다.
그리고 이 과정에서 구분자로 구분한 token들이 숫자값이 맞는지 확인하는 validator인 NumberValidator를 구현하였다.
package calculator.common;
/**
* 입력받은 배열 안의 값들이 숫자인지 검증하는 클래스
*/
public class NumberValidator {
public static void validateNumbers(String[] values) {
for (String value : values) {
if (value.isEmpty()) {
continue;
}
if (!value.matches("-?\\d+")) {
throw new IllegalArgumentException("Invalid number: " + value);
}
int num = Integer.parseInt(value);
if (num < 0) {
throw new IllegalArgumentException("Negative number not allowed: " + num);
}
}
}
}
개발 구조는 최대한 DDD 패턴을 사용하려고 했다. 그 이유는 유지보수성과 확장성을 고려했을 때, 코드가 비즈니스 로직 중심으로 조직되어 있어 기능 추가/수정 시 영향 범위가 제한되고, 새로운 기능이나 비즈니스 규칙이 추가될 때 도메인 모델 확장만으로 대응 가능할 것이라고 생각했다.
예를 들어 덧셈 연산에서 다른 뺄셈, 곱셈, 나눗셈 같은 연산이 추가되어도 그에 맞는 기능만 Calculator에 추가하면 되기 때문이다.
그 다음으로 Calculator입니다.
Calculator
먼저 readme를 작성해보겠습니다. Calculator는 아주 간단하다.
Calculator
1. 구분자로 나뉜 숫자들의 합을 계산
코드는 아래와 같다.
package calculator.domain;
import java.util.List;
/**
* 입력받은 숫자의 총합을 계산하는 클래스
*/
public class Calculator {
private int sum;
public Calculator() {
sum = 0;
}
public void sum(List<Integer> numbers) {
for (Integer number : numbers) {
sum += number;
}
}
public int getSum() {
return sum;
}
}
getSum()을 원래는 sum()으로 정의할까 했었지만 합을 계산하는 메서드인 sum(List<Integer> numbers)와 이름이 같아서 가독성을 위해서 getSum()으로 지정하였습니다. 어떤 이름이 더 좋을지 의견 주시는 분이 있다면 참고하고 싶다.
이렇게 DelimiterParser와 Calculator의 구현을 끝냈고, 테스트 코드를 작성하였다.
package calculator;
import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import camp.nextstep.edu.missionutils.test.NsTest;
import org.junit.jupiter.api.Test;
class ApplicationTest extends NsTest {
@Test
void 커스텀_구분자_사용() {
assertSimpleTest(() -> {
run("//;\\n1");
assertThat(output()).contains("결과 : 1");
});
}
@Test
void 예외_테스트() {
assertSimpleTest(() ->
assertThatThrownBy(() -> runException("-1,2,3"))
.isInstanceOf(IllegalArgumentException.class)
);
}
@Test
void 커스텀_구분자_다중_사용() {
assertSimpleTest(() -> {
run("//b\\n3,4,5b8b9//c\\n10c11");
assertThat(output()).contains("결과 : 50");
});
}
@Test
void 커스텀_구분자_다중_연속_사용() {
assertSimpleTest(() -> {
run("//'\\n//|\\n3'6|7");
assertThat(output()).contains("결과 : 16");
});
}
@Test
void 커스텀_구분자_숫자_사용() {
assertSimpleTest(() -> {
run("//5\\n3545");
assertThat(output()).contains("결과 : 7");
});
}
@Override
public void runMain() {
Application.main(new String[]{});
}
}
테스트 코드는 기존에 주어졌던 2개의 테스트에 더해, 3개의 테스트를 추가로 작성했다.
두 번째 테스트에서 입력값이 -1일 때 IllegalArgumentException이 발생하는 것을 확인하고, 음수 값은 입력으로 허용되지 않는다는 점을 알 수 있었다.
이후 사용자 지정 구분자에 대한 예외 테스트 2가지를 추가했고, 숫자가 구분자로 사용되는 경우에 대한 테스트도 작성했다.
그 외에도 다양한 예외 상황이 있을 수 있겠지만, 대부분은 위 5가지 경우와 유사하다고 판단하여 추가적인 테스트는 작성하지 않았다.
만약 이 외에 필요한 테스트 케이스가 있다면, 함께 토론해보는 것도 좋을 것 같다.
이번 과제를 진행하면서 잊고 있던 정규표현식을 다시 공부할 수 있었고, 자바 코드 컨벤션에 대해서도 더 깊이 이해하게 되어 많은 배움을 얻은 과제였다.
'우아한테크벨로 > 프리코스 회고록' 카테고리의 다른 글
| 우아안 테크코스[프리코스] 최종 코딩 테스트 회고록 (1) | 2026.01.23 |
|---|---|
| 우아한테크코스[프리코스] 5주차 회고록 (0) | 2025.11.20 |
| 우아한 테크코스[프리코스] 4주차 회고록 (0) | 2025.11.12 |
| 우아한 테크코스[프리코스] 3주차 회고록 (0) | 2025.11.04 |
| 우아한 테크코스[프리코스] 2주차 회고록 (0) | 2025.10.28 |
