Ch 1.3 자바 기본 구조와 문법
이제 본격적으로 자바 코드를 작성하고 실행하는 방법을 배워보겠습니다. 이 챕터에서는 자바 파일의 기본 구조부터 입출력, 에러 종류, 그리고 간단한 계산기 프로그램까지 다룹니다.
1. 자바 파일 구조 완전 해부
자바 소스 파일(.java)은 정해진 구조를 따릅니다. 아래 예제를 통해 각 부분의 역할을 이해해봅시다.
// 1. 패키지 선언 (선택, 최상단에 위치)
package com.example.myapp;
// 2. import 선언 (사용할 외부 클래스 가져오기)
import java.util.Scanner;
import java.util.ArrayList;
// 3. 클래스 선언 (파일명과 동일해야 함)
public class MyFirstProgram {
// 4. 필드(인스턴스 변수) 선언
private String programName = "나의 첫 프로그램";
// 5. 정적 필드(클래스 변수) 선언
private static int runCount = 0;
// 6. main 메서드 - 프로그램 진입점
public static void main(String[] args) {
// 7. 지역 변수 선언 및 사용
String message = "Hello, Java!";
System.out.println(message);
runCount++;
System.out.println("실행 횟수: " + runCount);
}
}
각 구성 요소 설명
| 구성 요소 | 위치 | 필수 여부 | 설명 |
|---|---|---|---|
package 선언 | 최상단 | 선택 | 클래스의 네임스페이스 정의 |
import 선언 | package 다음 | 선택 | 외부 클래스 사용 선언 |
| 클래스 선언 | 파일 본문 | 필수 | 모든 코드는 클래스 안에 있어야 함 |
main 메서드 | 클래스 안 | 실행 시 필수 | JVM이 찾는 프로그램 시작점 |
2. public static void main(String[] args) 완전 분석
이 긴 선언문은 자바에서 가장 중요한 구조입니다. 각 키워드의 의미를 하나씩 파악해봅시다.
public static void main(String[] args)
1 2 3 4 5
1. public (접근 제어자)
- 어디서든 이 메서드를 호출할 수 있다는 의미
- JVM이 프로그램 외부에서 main 메서드를 호출하므로 반드시
public이어야 함
2. static (정적 메서드)
- 객체를 생성하지 않고도 클래스에서 직접 호출할 수 있다는 의미
- JVM은 프로그램 시작 시 어떤 객체도 만들지 않고 main을 호출하므로 반드시
static이어야 함
3. void (반환 타입)
- 이 메서드가 아무것도 반환하지 않는다는 의미
- 프로그램 종료 코드는
System.exit(0)등으로 별도 처리
4. main (메서드 이름)
- JVM이 시작점으로 인식하는 정해진 이름
- 대소문자를 바꾸면(
Main,MAIN) JVM이 인식하지 못함
5. String[] args (매개변수)
- 명령줄에서 프로그램 실행 시 전달되는 인수를 String 배열로 받음
args는 관례적인 이름이며arguments의 줄임말
public class CommandLineArgs {
public static void main(String[] args) {
System.out.println("전달된 인수 개수: " + args.length);
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
실행 예:
java CommandLineArgs Hello World 123
# 전달된 인수 개수: 3
# args[0] = Hello
# args[1] = World
# args[2] = 123
Java 21부터는 단일 파일 프로그램에서 클래스 선언 없이 main 메서드만 작성하는 간소화된 main 메서드(Preview 기능) 를 지원하기 시작했습니다. 하지만 표준 형태를 먼저 익히는 것이 중요합니다.
3. 주석 (Comment)
주석은 코드에 설명을 남기는 방법입니다. 컴파일러가 무시하므로 프로그램 실행에 영향을 주지 않습니다.
3.1 한 줄 주석 (//)
public class CommentExample {
public static void main(String[] args) {
// 이것은 한 줄 주석입니다
int age = 25; // 나이 변수 선언 - 줄 끝에도 사용 가능
// System.out.println("이 줄은 실행되지 않습니다");
System.out.println("나이: " + age);
}
}
3.2 여러 줄 주석 (/* */)
public class MultiLineComment {
public static void main(String[] args) {
/*
* 여러 줄에 걸친 주석입니다.
* 코드 블록을 임시로 비활성화할 때 자주 사용합니다.
* 각 줄 앞의 * 는 관례적 표기이며 필수는 아닙니다.
*/
int x = 10;
int y = 20;
/* int z = x + y; -- 임시로 비활성화 */
System.out.println(x + y);
}
}
3.3 JavaDoc 주석 (/** */)
API 문서를 자동 생성하는 데 사용합니다. javadoc 도구가 이 주석을 읽어 HTML 문서를 만듭니다.
/**
* 사각형의 넓이를 계산하는 클래스입니다.
*
* @author 홍길동
* @version 1.0
* @since 2024-01-01
*/
public class Rectangle {
/**
* 너비와 높이를 받아 사각형의 넓이를 계산합니다.
*
* @param width 사각형의 너비 (양수여야 함)
* @param height 사각형의 높이 (양수여야 함)
* @return 계산된 넓이 (너비 × 높이)
* @throws IllegalArgumentException 너비 또는 높이가 0 이하인 경우
*/
public static double calculateArea(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("너비와 높이는 양수여야 합니다.");
}
return width * height;
}
public static void main(String[] args) {
double area = calculateArea(5.0, 3.0);
System.out.println("넓이: " + area); // 15.0
}
}
주요 JavaDoc 태그:
| 태그 | 설명 | 예시 |
|---|---|---|
@param | 매개변수 설명 | @param name 사용자 이름 |
@return | 반환값 설명 | @return 계산 결과 |
@throws | 발생 가능한 예외 | @throws NullPointerException |
@author | 작성자 | @author 홍길동 |
@version | 버전 | @version 2.1 |
@since | 추가된 버전 | @since Java 8 |
@see | 참고 항목 | @see String#length() |
@deprecated | 사용 중단 표시 | @deprecated 다음 버전에서 제거 |
4. 명명 규칙 (Naming Conventions)
자바는 업계 표준 명명 규칙을 따릅니다. 이 규칙을 지키면 다른 개발자가 코드를 읽기 쉬워집니다.
4.1 camelCase - 변수, 메서드
첫 단어는 소문자, 이후 단어의 첫 글자는 대문자
// 변수 이름 (camelCase)
int studentAge = 20;
String firstName = "길동";
boolean isLoggedIn = false;
double monthlyIncome = 3500000.0;
// 메서드 이름 (camelCase)
public void printStudentInfo() { }
public int calculateTotalScore() { return 0; }
public boolean isValidEmail(String email) { return true; }
4.2 PascalCase - 클래스, 인터페이스, Enum
모든 단어의 첫 글자를 대문자로 (UpperCamelCase라고도 함)
// 클래스 이름 (PascalCase)
public class StudentManager { }
public class HttpRequestHandler { }
// 인터페이스 이름 (PascalCase)
public interface Printable { }
public interface DataProcessor { }
// Enum 이름 (PascalCase)
public enum DayOfWeek { MONDAY, TUESDAY, WEDNESDAY }
4.3 UPPER_SNAKE_CASE - 상수
모든 문자 대문자, 단어 구분은 언더스코어(_)
// 상수 (static final 변수)
public static final double PI = 3.141592653589793;
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_CHARSET = "UTF-8";
public static final int HTTP_OK = 200;
4.4 패키지 이름 - 소문자
package com.mycompany.project.util; // 모두 소문자
package kr.co.example.service; // 도메인 역순
명명 규칙 종합 예제:
package com.example.school; // 패키지: 소문자
public class StudentGradeManager { // 클래스: PascalCase
private static final int MAX_SCORE = 100; // 상수: UPPER_SNAKE_CASE
private static final String PASS_GRADE = "합격";
private String studentName; // 변수: camelCase
private int examScore;
private boolean hasPassed;
public StudentGradeManager(String studentName, int examScore) {
this.studentName = studentName;
this.examScore = examScore;
this.hasPassed = examScore >= 60;
}
public void printGradeReport() { // 메서드: camelCase
String result = hasPassed ? PASS_GRADE : "불합격";
System.out.printf("%s: %d점 (%s)%n", studentName, examScore, result);
}
public static void main(String[] args) {
StudentGradeManager student = new StudentGradeManager("홍길동", 85);
student.printGradeReport(); // 홍길동: 85점 (합격)
}
}
자바 명명 규칙을 지키지 않아도 컴파일은 되지만, 팀 프로젝트에서 코드 리뷰 시 지적을 받거나 코드 품질 도구(Checkstyle, SonarQube)에서 경고가 발생합니다. 처음부터 습관을 들이는 것이 좋습니다.
5. 자바 코딩 컨벤션
5.1 들여쓰기
4칸 공백(스페이스)을 사용합니다. 탭 대신 공백을 권장합니다.
public class IndentExample {
public static void main(String[] args) {
if (true) { // 4칸 들여쓰기
System.out.println("if");
if (true) {
System.out.println("중첩 if"); // 8칸 들여쓰기
}
}
}
}
5.2 중괄호 위치
여는 중괄호 {는 같은 줄 끝에, 닫는 중괄호 }는 새 줄에 작성합니다 (K&R 스타일).
// 올바른 방법 (K&R 스타일)
public void goodStyle() {
if (condition) {
doSomething();
} else {
doOther();
}
}
// 지양하는 방법 (Allman 스타일 - 자바에서는 비표준)
public void differentStyle()
{
if (condition)
{
doSomething();
}
}
5.3 공백
// 연산자 양쪽에 공백
int result = a + b; // 좋음
int result = a+b; // 지양
// 콤마 뒤에 공백
method(arg1, arg2, arg3); // 좋음
method(arg1,arg2,arg3); // 지양
// 메서드 이름과 괄호 사이에 공백 없음
void myMethod() { } // 좋음
void myMethod () { } // 지양
6. 출력 메서드 완전 정리
6.1 세 가지 출력 메서드 비교
public class PrintComparison {
public static void main(String[] args) {
// 1. System.out.println: 출력 후 줄바꿈
System.out.println("첫 번째 줄");
System.out.println("두 번째 줄");
// 출력:
// 첫 번째 줄
// 두 번째 줄
// 2. System.out.print: 출력 후 줄바꿈 없음
System.out.print("A");
System.out.print("B");
System.out.print("C");
System.out.println(); // 수동 줄바꿈
// 출력: ABC
// 3. System.out.printf: C언어 방식 포맷 출력 (줄바꿈 없음)
System.out.printf("이름: %s, 나이: %d%n", "홍길동", 25);
// 출력: 이름: 홍길동, 나이: 25
}
}
6.2 printf 포맷 지정자
printf는 C언어에서 가져온 형식화 출력 방법입니다.
| 지정자 | 타입 | 설명 | 예시 |
|---|---|---|---|
%d | 정수 | 10진수 정수 | printf("%d", 42) → 42 |
%f | 실수 | 부동소수점 | printf("%f", 3.14) → 3.140000 |
%.2f | 실수 | 소수점 2자리 | printf("%.2f", 3.14159) → 3.14 |
%s | 문자열 | 문자열 | printf("%s", "Java") → Java |
%c | 문자 | 단일 문자 | printf("%c", 'A') → A |
%b | 불리언 | true/false | printf("%b", true) → true |
%n | 줄바꿈 | OS 독립적 개행 | printf("Hi%n") → Hi\n |
%10d | 정수 | 10자리 오른쪽 정렬 | printf("%10d", 42) → 42 |
%-10d | 정수 | 10자리 왼쪽 정렬 | printf("%-10d", 42) → 42 |
%05d | 정수 | 5자리, 빈자리는 0 | printf("%05d", 42) → 00042 |
public class PrintfExample {
public static void main(String[] args) {
String name = "김철수";
int age = 28;
double height = 175.5;
double score = 95.678;
// 기본 포맷
System.out.printf("이름: %s%n", name);
System.out.printf("나이: %d세%n", age);
System.out.printf("키: %.1fcm%n", height);
// 소수점 자릿수 제어
System.out.printf("점수: %.2f%n", score); // 95.68
// 정렬과 패딩
System.out.printf("|%10s|%-10s|%n", "오른쪽", "왼쪽");
System.out.printf("|%10d|%-10d|%n", 42, 42);
System.out.printf("|%010d|%n", 42); // |0000000042|
// 성적표 예제
System.out.println("=== 성적표 ===");
System.out.printf("%-8s %5s %5s%n", "이름", "점수", "등급");
System.out.printf("%-8s %5d %5s%n", "홍길동", 95, "A");
System.out.printf("%-8s %5d %5s%n", "김영희", 82, "B");
System.out.printf("%-8s %5d %5s%n", "이철수", 71, "C");
}
}
출력 결과:
이름: 김철수
나이: 28세
키: 175.5cm
점수: 95.68
| 오른쪽|왼쪽 |
| 42|42 |
|0000000042|
=== 성적표 ===
이름 점수 등급
홍길동 95 A
김영희 82 B
이철수 71 C
7. Scanner를 이용한 사용자 입력
Scanner 클래스를 사용하면 콘솔에서 사용자 입력을 받을 수 있습니다.
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
// Scanner 객체 생성 (System.in = 표준 입력 스트림)
Scanner scanner = new Scanner(System.in);
System.out.print("이름을 입력하세요: ");
String name = scanner.nextLine(); // 한 줄 전체 읽기
System.out.print("나이를 입력하세요: ");
int age = scanner.nextInt(); // 정수 읽기
System.out.print("키를 입력하세요 (cm): ");
double height = scanner.nextDouble(); // 실수 읽기
System.out.printf("안녕하세요, %s님! 나이 %d세, 키 %.1fcm이시군요.%n",
name, age, height);
// 사용 후 Scanner 닫기 (자원 해제)
scanner.close();
}
}
주요 Scanner 메서드:
| 메서드 | 읽는 타입 | 설명 |
|---|---|---|
next() | String | 공백 전까지 한 토큰 읽기 |
nextLine() | String | 줄바꿈 전까지 전체 읽기 |
nextInt() | int | 정수 읽기 |
nextDouble() | double | 실수 읽기 |
nextBoolean() | boolean | true/false 읽기 |
nextLong() | long | 큰 정수 읽기 |
hasNext() | boolean | 다음 토큰 존재 여부 확인 |
nextInt() 또는 nextDouble() 뒤에 nextLine()을 호출하면 빈 문자열이 읽힙니다. 이는 숫자 입력 후 남아있는 줄바꿈 문자 때문입니다.
int age = scanner.nextInt();
scanner.nextLine(); // 버퍼에 남은 개행 문자 소비
String name = scanner.nextLine(); // 이제 정상적으로 읽힘
8. 변수 선언과 초기화
자바는 강한 타입 언어 로, 모든 변수는 사용 전 타입을 선언해야 합니다.
public class VariableExample {
public static void main(String[] args) {
// 선언과 초기화를 동시에
int count = 0;
String greeting = "안녕하세요";
double pi = 3.14159;
boolean isActive = true;
// 선언 후 나중에 초기화
int result;
result = 100; // 사용 전에 반드시 초기화
// 여러 변수를 한 줄에 선언 (같은 타입만 가능, 권장하지 않음)
int x = 1, y = 2, z = 3;
// Java 10+ var 키워드 (타입 추론)
var message = "Java 10부터 사용 가능"; // String으로 추론
var number = 42; // int로 추론
System.out.println(count + " " + greeting);
System.out.println("pi = " + pi);
System.out.println("result = " + result);
}
}
기본 데이터 타입 (Primitive Types)
| 타입 | 크기 | 기본값 | 범위 | 예시 |
|---|---|---|---|---|
byte | 1 byte | 0 | -128 ~ 127 | byte b = 100; |
short | 2 byte | 0 | -32768 ~ 32767 | short s = 1000; |
int | 4 byte | 0 | 약 ±21억 | int i = 100000; |
long | 8 byte | 0L | 약 ±922경 | long l = 100L; |
float | 4 byte | 0.0f | 소수점 약 7자리 | float f = 3.14f; |
double | 8 byte | 0.0 | 소수점 약 15자리 | double d = 3.14; |
char | 2 byte | '\u0000' | 0 ~ 65535 (유니코드) | char c = 'A'; |
boolean | 1 bit | false | true / false | boolean b = true; |
9. 표현식(Expression)과 문장(Statement)
표현식 (Expression)
값을 계산하거나 반환하는 코드 조각입니다.
// 산술 표현식
3 + 4 // 값: 7
x * y // 값: x와 y의 곱
a > b // 값: true 또는 false
// 메서드 호출 표현식
Math.max(10, 20) // 값: 20
"Hello".length() // 값: 5
// 대입 표현식
x = 5 // 값: 5 (대입 후 x의 값)
문장 (Statement)
실행 가능한 최소 코드 단위로, 세미콜론(;)으로 끝납니다.
public class StatementExample {
public static void main(String[] args) {
// 선언문
int x = 10;
// 표현식 문장 (expression statement)
x = 20; // 대입
x++; // 증가 연산
System.out.println(x); // 메서드 호출
// 제어문 (블록을 포함, 세미콜론 불필요)
if (x > 15) {
System.out.println("15보다 큽니다");
}
// 블록 문장 (여러 문장을 중괄호로 묶음)
{
int temp = 100;
System.out.println("블록 안: " + temp);
}
// temp는 여기서 접근 불가 (블록 스코프)
}
}
10. 에러(오류)의 세 가지 종류
자바에서 발생하는 오류는 발생 시점에 따라 세 가지로 분류됩니다.
10.1 컴파일 에러 (Compile-time Error)
소스코드를 바이트코드로 변환하는 컴파일 단계 에서 발생합니다. IDE나 javac가 즉시 알려줘서 가장 잡기 쉬운 에러입니다.
public class CompileErrorExample {
public static void main(String[] args) {
// 컴파일 에러 예시들:
int x = "Hello"; // 타입 불일치 오류
// → error: incompatible types: String cannot be converted to int
System.out.println(y); // 선언하지 않은 변수 사용
// → error: cannot find symbol
if (true) // 세미콜론 누락 (바로 다음 줄에서 오류)
System.out.println("OK")
// → error: ';' expected
}
}
10.2 런타임 에러 (Runtime Error)
프로그램이 실행되는 도중 발생하는 에러입니다. 컴파일은 성공하지만 실행 시 예외(Exception)가 발생합니다.
public class RuntimeErrorExample {
public static void main(String[] args) {
// 1. NullPointerException: null 참조에 메서드/필드 접근
String s = null;
System.out.println(s.length()); // NullPointerException 발생!
// 2. ArrayIndexOutOfBoundsException: 배열 범위 초과 접근
int[] arr = new int[3]; // 인덱스 0, 1, 2만 유효
arr[5] = 10; // ArrayIndexOutOfBoundsException 발생!
// 3. NumberFormatException: 잘못된 형식으로 변환
int n = Integer.parseInt("abc"); // NumberFormatException 발생!
// 4. ArithmeticException: 0으로 나누기
int result = 10 / 0; // ArithmeticException 발생!
}
}
10.3 논리 에러 (Logic Error)
컴파일과 실행은 모두 성공하지만 결과가 의도와 다른 에러입니다. 가장 발견하기 어렵습니다.
public class LogicErrorExample {
public static void main(String[] args) {
// 섭씨를 화씨로 변환하는 공식: F = C * 9/5 + 32
double celsius = 100.0;
// 논리 에러: 정수 나누기로 인해 9/5가 1이 됨
double fahrenheit = celsius * 9/5 + 32; // 잘못된 계산: 132.0
// 올바른 계산: celsius * 9.0/5 + 32 → 212.0
System.out.println(celsius + "°C = " + fahrenheit + "°F");
// 출력: 100.0°C = 132.0°F (오답! 정답은 212.0°F)
}
}
에러 비교 요약:
| 종류 | 발생 시점 | 발견 용이성 | 예시 |
|---|---|---|---|
| 컴파일 에러 | javac 실행 시 | 매우 쉬움 (IDE가 즉시 표시) | 문법 오류, 타입 불일치 |
| 런타임 에러 | 프로그램 실행 중 | 보통 (스택 트레이스 제공) | NullPointerException |
| 논리 에러 | 결과 확인 후 | 어려움 (디버거 필요) | 잘못된 알고리즘 |
11. 실전 예제: 사칙연산 계산기
지금까지 배운 내용을 종합한 완전한 계산기 프로그램입니다.
import java.util.Scanner;
/**
* 사칙연산 계산기
* 사용자로부터 두 숫자와 연산자를 입력받아 결과를 출력합니다.
*/
public class Calculator {
/**
* 두 정수의 덧셈 결과를 반환합니다.
*/
public static double add(double a, double b) {
return a + b;
}
/**
* 두 정수의 뺄셈 결과를 반환합니다.
*/
public static double subtract(double a, double b) {
return a - b;
}
/**
* 두 정수의 곱셈 결과를 반환합니다.
*/
public static double multiply(double a, double b) {
return a * b;
}
/**
* 두 정수의 나눗셈 결과를 반환합니다.
*
* @throws ArithmeticException 0으로 나누려 할 때 발생
*/
public static double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("0으로 나눌 수 없습니다!");
}
return a / b;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("===== 사칙연산 계산기 =====");
System.out.println("연산자: + (덧셈), - (뺄셈), * (곱셈), / (나눗셈)");
System.out.println();
// 첫 번째 숫자 입력
System.out.print("첫 번째 숫자를 입력하세요: ");
double num1 = scanner.nextDouble();
// 연산자 입력
System.out.print("연산자를 입력하세요 (+, -, *, /): ");
String operator = scanner.next();
// 두 번째 숫자 입력
System.out.print("두 번째 숫자를 입력하세요: ");
double num2 = scanner.nextDouble();
// 계산 수행
double result;
boolean isValid = true;
switch (operator) {
case "+":
result = add(num1, num2);
break;
case "-":
result = subtract(num1, num2);
break;
case "*":
result = multiply(num1, num2);
break;
case "/":
if (num2 == 0) {
System.out.println("오류: 0으로 나눌 수 없습니다!");
isValid = false;
result = 0;
} else {
result = divide(num1, num2);
}
break;
default:
System.out.println("오류: 올바르지 않은 연산자입니다 '" + operator + "'");
isValid = false;
result = 0;
}
// 결과 출력
if (isValid) {
System.out.println();
System.out.println("===== 계산 결과 =====");
// 정수로 딱 떨어지면 .0 없이 출력
if (result == (long) result) {
System.out.printf("%.0f %s %.0f = %.0f%n",
num1, operator, num2, result);
} else {
System.out.printf("%.2f %s %.2f = %.4f%n",
num1, operator, num2, result);
}
}
scanner.close();
}
}
실행 예시 1 (덧셈):
===== 사칙연산 계산기 =====
연산자: + (덧셈), - (뺄셈), * (곱셈), / (나눗셈)
첫 번째 숫자를 입력하세요: 15
연산자를 입력하세요 (+, -, *, /): +
두 번째 숫자를 입력하세요: 27
===== 계산 결과 =====
15 + 27 = 42
실행 예시 2 (나눗셈):
첫 번째 숫자를 입력하세요: 10
연산자를 입력하세요 (+, -, *, /): /
두 번째 숫자를 입력하세요: 3
===== 계산 결과 =====
10.00 / 3.00 = 3.3333
실행 예시 3 (0으로 나누기):
첫 번째 숫자를 입력하세요: 5
연산자를 입력하세요 (+, -, *, /): /
두 번째 숫자를 입력하세요: 0
오류: 0으로 나눌 수 없습니다!
IntelliJ 단축키 팁:
psvm+ Tab:public static void main(String[] args) {}자동 완성sout+ Tab:System.out.println()자동 완성souf+ Tab:System.out.printf()자동 완성Ctrl + Shift + F10: 현재 파일 실행Ctrl + /: 선택된 줄 주석 처리/해제Alt + Enter: 오류에 대한 빠른 수정 제안 표시
요약
| 항목 | 핵심 내용 |
|---|---|
| 파일 구조 | package → import → class → method 순서 |
| main 메서드 | public static void main(String[] args) - JVM 진입점 |
| 주석 | // 한 줄, /* */ 여러 줄, /** */ JavaDoc |
| 명명 규칙 | 변수/메서드: camelCase, 클래스: PascalCase, 상수: UPPER_SNAKE_CASE |
| 출력 | println(줄바꿈), print(줄바꿈 없음), printf(포맷 지정) |
| 입력 | Scanner 클래스로 표준 입력 처리 |
| 에러 종류 | 컴파일 에러, 런타임 에러, 논리 에러 |