
내가 생각했을때 문제에서 원하는부분
Input to this problem will consist of a (non-empty) series of up to 100 data sets.
Each data set will be formatted according to the following description, and there will be no blank lines separating data sets.
All characters will be uppercase.
A single data set has 3 components:
Start line – A single line, “START”
Cipher message – A single line containing from one to two hundred characters, inclusive, comprising a single message from Caesar
End line – A single line, “END”
Following the final data set will be a single line, “ENDOFINPUT”.
For each data set, there will be exactly one line of output.
This is the original message by Caesar.
내가 이 문제를 보고 생각해본 부분
입력 스트림과 결과 저장 공간 초기화:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));:
이 부분은 키보드(표준 입력)로부터 데이터를 읽기 위한 통로를 만드는 과정이다.
System.in은 바이트 단위로 데이터를 읽는 반면, 우리는 문자를 다루기 때문에 InputStreamReader를 통해 System.in을 문자 스트림으로 변환해 준다.
BufferedReader는 이렇게 변환된 문자 스트림을 효율적으로 읽을 수 있도록 도와준다.
한 줄씩 통째로 읽는 readLine()이라는 아주 편리한 기능을 제공한다.
이 문제처럼 한 줄 단위의 입력을 처리할 때 아주 유용한다.
StringBuilder sb = new StringBuilder();:
이 sb는 복호화된 모든 메시지, 즉 최종 결과들을 한데 모아서 저장할 변수이다.
왜 String 대신 StringBuilder를 사용할까?
Java에서 String 객체는 한 번 만들어지면 내용을 바꿀 수 없는 "불변" 객체이다.
만약 복호화된 결과들을 String에 + 연산자로 계속 더하면, 매번 새로운 String 객체가 생성되면서 메모리도 낭비되고 성능도 느려질 수 있다.
하지만 StringBuilder는 "가변" 객체라서 내용을 추가하거나 변경해도 새로운 객체를 만들지 않고 효율적으로 처리한다.
여러 개의 메시지를 모아서 한 번에 출력해야 할 때 이처럼 StringBuilder를 사용하는 것이 아주 좋은 습관이다.
모든 데이터 세트 처리: while 반복문
이 while 반복문은 문제에서 제시된 여러 개의 데이터 세트(테스트 케이스)를 하나씩 처리하는 전체 흐름을 담당한다.
(line = br.readLine()) != null:
br.readLine()은 입력에서 한 줄을 읽어 line 변수에 할당한다.
그리고 이 line이 null이 아닌지 확인한다.
입력이 끝났을 때(EOF, End-Of-File) readLine()은 null을 반환한다.
null을 만나면 더 이상 읽을 것이 없으니 반복을 멈추게 된다.
!line.equals("ENDOFINPUT"):
문제에서 "ENDOFINPUT"이라는 특별한 문자열이 나오면 입력의 끝이라고 명시되어 있다.
그래서 readLine()으로 읽은 줄이 "ENDOFINPUT"이 아닌 경우에만 계속 반복하도록 조건을 달았다.
이 두 가지 조건이 모두 참일 때만 반복문 안의 내용이 실행된다.
각 암호문 세트 처리: "START" 블록
if (line.equals("START")): while 루프에서 읽은 line이 "START"와 정확히 일치할 때만 이 안의 블록이 실행된다.
"START"는 새로운 암호문 세트가 시작됨을 의미한다.
String cipherMessage = br.readLine();: "START" 다음 줄에는 암호화된 본문 메시지가 나타난다.
이 줄을 읽어서 cipherMessage라는 변수에 저장한다.
이제 이 cipherMessage를 가지고 복호화를 시작할 거다.
br.readLine();: cipherMessage 다음 줄은 항상 "END"이다.
이 "END" 라인은 복호화에 필요 없으므로, 그냥 한 줄 읽어 들여서 버린다.
StringBuilder decryptedMessage = new StringBuilder();: 이 StringBuilder는 현재 처리 중인 하나의 암호문을 복호화하여 저장하기 위한 임시 공간이다.
매 "START" 블록마다 새로 만들어져서 해당 데이터 세트의 평문만 담게 된다.
sb.append(decryptedMessage.toString()).append("\n");: 한 데이터 세트의 복호화가 완전히 끝나면, decryptedMessage에 모아진 평문을 String으로 변환해서 전체 결과 sb에 추가한다.
append("\n")을 통해 각 복호화 결과 뒤에 줄바꿈을 넣어주어 문제의 출력 형식에 맞춘다.
실제 암호문 복호화: for 루프와 시저 암호 규칙
for (char ch : cipherMessage.toCharArray()): cipherMessage에 저장된 암호문을 char 배열로 바꾼 다음, 배열의 각 문자를 ch라는 변수에 하나씩 넣어주며 반복한다.
이렇게 하면 암호문의 모든 문자를 하나씩 검사하고 변환할 수 있다.
if (ch >= 'A' && ch <= 'Z'): 현재 문자인 ch가 알파벳 대문자인지 확인하는 조건이다.
시저 암호는 알파벳에만 적용되므로, 이 조건은 매우 중요하다.
char decryptedChar = (char) (ch - 5);: ch가 알파벳 대문자인 경우, 복호화를 위해 ASCII 값에서 5를 뺀다.
문제에서는 오른쪽으로 5칸 밀어 암호화했으므로, 우리는 반대로 왼쪽으로 5칸 이동시켜 원래 글자를 찾는다.
ch - 5의 결과는 정수(int)이므로, 다시 문자(char) 타입으로 강제 형변환((char))해준다.
if (decryptedChar < 'A') { decryptedChar = (char) (decryptedChar + 26); }:
이 부분이 시저 암호의 "순환(wrap-around)" 특성을 처리하는 핵심이다.
예를 들어 암호문이 'A' (ASCII 값 65)라고 해본다.
여기서 5를 빼면 60이 되는데, 이 값은 알파벳 범위 밖이다.
이럴 때, 알파벳 전체 개수인 26을 더해주면 다시 알파벳 범위 안으로 들어오게 된다.
즉, 60 + 26 = 86이 되는데, 이 86은 ASCII에서 'V'를 의미한다.
그래서 'A'가 'V'로 복호화되는 거다.
이 규칙이 문제에서 제시된 표(A->V, B->W, ...)와 정확히 일치한다.
decryptedMessage.append(decryptedChar);: 계산된 복호화 문자를 decryptedMessage에 추가한다.
else { decryptedMessage.append(ch); }: 만약 ch가 알파벳 대문자가 아니라면(예: 공백, 숫자, 특수 문자 등), 이들은 시저 암호의 대상이 아니므로 변경 없이 decryptedMessage에 그대로 추가한다.
최종 결과 출력 및 자원 해제
System.out.print(sb.toString());: while 루프가 모두 완료되고 모든 암호문 세트의 복호화가 끝났다면, 최종 결과 sb에 저장된 모든 평문들을 하나의 거대한 String으로 만들어서 콘솔에 출력한다.
print를 사용하면 append("\n")으로 이미 추가된 줄바꿈만 적용된다.
br.close();: BufferedReader와 같은 입력/출력 스트림은 운영체제의 자원을 사용한다.
코드로 구현
package baekjoon.baekjoon_32;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
// 백준 9971번 문제
public class Main1271 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 여러 테스트 케이스의 결과를 효율적으로 저장하기 위해 StringBuilder를 사용합니다.
StringBuilder sb = new StringBuilder();
String line;
// 입력의 마지막에 "ENDOFINPUT"이 올 때까지 반복합니다.
while ((line = br.readLine()) != null && !line.equals("ENDOFINPUT")) {
// "START" 라인을 만나면 실제 암호문 처리 로직을 시작합니다.
if (line.equals("START")) {
String cipherMessage = br.readLine(); // 암호문을 읽어옵니다.
br.readLine(); // 다음 라인인 "END"는 읽어서 버립니다.
StringBuilder decryptedMessage = new StringBuilder(); // 복호화된 메시지를 저장할 StringBuilder
// 암호문의 각 문자를 순회하며 처리합니다.
for (char ch : cipherMessage.toCharArray()) {
// 현재 문자가 알파벳 대문자인지 확인합니다.
if (ch >= 'A' && ch <= 'Z') {
// 알파벳인 경우 왼쪽으로 5칸 이동합니다.
char decryptedChar = (char) (ch - 5);
// 만약 'A'보다 작아져서 범위를 벗어나면, 'Z' 뒤부터 다시 계산합니다.
// 예를 들어, 'A'는 ASCII 65, 'B'는 66. 'A'-5는 60.
// 60은 'A'(65)보다 작으므로 +26을 하여 86(V)으로 만듭니다.
if (decryptedChar < 'A') {
decryptedChar = (char) (decryptedChar + 26);
}
decryptedMessage.append(decryptedChar); // 복호화된 문자를 추가합니다.
} else {
// 알파벳이 아닌 문자는 그대로 추가합니다.
decryptedMessage.append(ch);
}
}
// 하나의 테스트 케이스 처리가 끝나면 결과를 resultBuilder에 추가하고 줄바꿈합니다.
sb.append(decryptedMessage.toString()).append("\n");
}
}
// 모든 테스트 케이스 처리 후 최종 결과를 한 번에 출력합니다.
System.out.print(sb.toString());
br.close(); // BufferedReader를 닫아 자원을 해제합니다.
}
}
코드와 설명이 부족할수 있습니다. 코드를 보시고 문제가 있거나 코드 개선이 필요한 부분이 있다면 댓글로 말해주시면 감사한 마음으로 참고해 코드를 수정 하겠습니다.