최근 Java 문법에 대해 책을 정독하며 공부하며 이번에 처음 알게 되어 한번은 정리해놓고 싶은 개념들을 정리해보려고 합니다. 학교에서 한 학기 동안 Java 프로그래밍을 들으며 학습했던 내용 외에는 Java 언어에 대해 직접 책을 정독하며 공부한 적이 없었는데, 그만큼 지식에 많은 빈틈을 갖고 있었고 이번 기회에 이렇게 학습하고 기록할 수 있다는 점에서 다행이라고 생각합니다.
리터럴 정의
리터럴은 너무 간단한데 용어 자체를 이번에 처음으로 알게되었다. 기본형 타입에 값을 넣어서 선언한 경우를 리터럴이라고 부른다. String 같은 경우 참조 타입 리터럴로 Heap에 String 상수 풀을 관리하며 동일한 문자열 리터럴에 대해서 동일한 객체를 참조한다.
대학교에서 C를 기반으로 학습을 진행했어서 그런지 이런 기본적인 용어에 대해 무지하다는 사실을 이번에 처음 깨달을 수 있었다.
리터럴에서 하나 더 배운것은 아래처럼 int 범위를 벗어나는 숫자를 입력하면서 L을 붙여주지 않으면 컴파일러가 정수 리터럴로 판단해 컴파일 오류로 표현한다는 사실이다.

부동 소수점을 더 자세히..
가장 처음 소수 계산에 대해 오차가 있다는 사실을 알게 된 것은 알고리즘 문제풀이에서였다. 하지만 그때는 float은 7자리 double은 15자리라는 기준만 외우고 더 자세하게 공부할 생각을 갖지 않았다. 이번에 한번 그래도 원리를 이해해보고 싶어 공부를 해봤는데 역시 어려운 내용이라고 생각이 들었다. 그래도 이해한 내용을 이번에 정리하지 않으면 다음에 또 까먹지 않을까?

위 그림은 위키피디아 float(4byte)를 비트로 표현한 것이다. 앞 1비트는 음수 표현이고 중간 8비트는 지수부, 마지막 23비트는 가수 부라고 부른다.
정수를 생각해보면 이진법의 덧셈 표현으로 모든 정수를 표현할 수 있다. 아래 그림을 보면 2의 제곱들로 다른 모든 정수를 표현할 수 있다.

하지만 이진법을 이용해 소수를 표현하려고 하면 0.5, 0.25, 0.125...로 이를 모두 더해서 모든 소수를 표현할 수 없다. 이런 부분에서 발생하는 것이 바로 유효숫자이다. 이를 이렇게 개념적으로만 접근하는 것이 정신건강에는 좋은 것 같다 😂

이게 십진법으로표현된 표현할 수 있는 숫자에 대해 부동소수점으로 어떻게 나오는 건지 계산하는 과정인데 수학 공식을 외우듯이 규칙을 암기하며 보면 조금은 그래도 볼 만하다고 생각한다.
다재다능한 Enum
Java Enum 활용기 | 우아한형제들 기술블로그
안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 Enum에 관해
techblog.woowahan.com
위 블로그글에서 Enum으로 상태와 행위를 한곳에서 관리하는 부분을 보고 머리를 한 대 맞는 기분이 들었다. 그래서 이 내용을 "블로그 글에 정리해 두고 두고두고 이 패턴을 사용할 수 있는 곳을 발견하면 바로 사용해야지!"라는 생각을 갖게 되었다. Enum을 지금까지 Type을 위한 용도로만 사용해 왔다. 계산기에 Enum을 사용한다면 아래와 같이 선언해서 사용해 볼 수 있다고 생각한다.
enum CalcType{
PLUS,
MINUS
}
이렇게 진행하면 문제는 실제 계산을 진행하는 부분이 다음과 같이 if문이 생기고 더많은 연산이 추가되면 추가되는 만큼 if문도 증가하게 된다.
public static int calculate(String code, int a, int b){
if(code.equals(CalcType.PLUS)){
return a + b;
}
else if (code.equals(CalcType.MINUS)){
return a-b;
}
}
따라서 이런 덧셈이라는 행위 자체도 Enum에 저장해 if를 제거하는 것이다.
enum CalcType{
PLUS((a,b)->a+b),
MINUS((a,b)->a-b);
BiFunction<Integer, Integer, Integer> func;
CalcType(BiFunction<Integer, Integer, Integer> func) {
this.func = func;
}
public int calculate(Integer a, Integer b) {
return func.apply(a, b);
}
}
위 코드처럼 작성하면 if문은 다음과 같이 제거할 수 있다.
public static int calculate(String code, int a, int b){
code.calculate(a,b);
}
이 예시는 빠르게 작성하려고 만든 간단한 예시라 실제와는 많이 다를 수 있는데 Type을 사용하는 프로젝트 코드에도 한번 도입해 봐야겠다는 생각이 바로 들었다.
4개의 접근제한자와 패키지
지금까지 접근제한자는 3가지라고 생각하고 있었다. 그런데 default라는 접근제한자가 하나 더 있었고 이에 대해 내용을 정리하며 protected의 기능도 추가적으로 공부하게 되었다.
클래스 내부에서 메서드에 default로 접근제한자를 붙이면 해당 메서드는 같은 패키지 내부에 있는 클래스에서만 사용할 수 있다.
여기서 같은 패키지의 범위는 무엇일까?

위 사진의 클래스 A의 패키지는 com.example.demo.apcak 이다. 이렇게 아무 한정자도 작성하지 않으면 자동으로 default가 붙게 되고 아래 사진처럼 다른 패키지의 클래스에서 display()를 호출하려고 하면 컴파일 에러가 난다.


protected의 경우도 지금까지는 상속해서 자식 클래스만 사용할 수 있는 것으로 알고 있었는데 같은 패키지 내에 존재하는 다른클래스가 호출할 수 있다는 사실을 새로이 알게 되었다.


이 패키지의 개념이 바이트코드(. class)로 java파일을 변환할 때도 등장하는데 인텔리제이로 개발하는 경우 out폴더에 javac 결과로 생성된 바이트 코드들이 존재하는 것을 볼 수 있다. 이때, java 명령어로 main 메서드를 실행할 때 classpath를
`-classpath C:\Users\ahc70\Desktop\java-ex\out\production\java-ex` 이렇게 인텔리제이에서 주고 있었는데 실제 com.example.demo.apack은 java-ex/폴더 안에 존재하고 있었다.

즉, 우리가 class 위에 적던 package 이름은 그냥 적는 게 아니라 이렇게 컴파일하고 실행할 때 JVM에게 이 클래스는 여기 있어라고 알려주는 용도로 사용되고 있었다. 나중에 리플렉션에 대해 배울 때, 클래스 정보를 뽑아내면 클래스 이름뿐만 아니라 패키지 이름까지 나오는 것을 보고 패키지의 중요성에 대해서 생각해 볼 수 있는 시간이었다.
오버라이딩과 리스코프 치환원칙
리스코프 치환원칙은 지금까지 그냥 다형성(부모타입 선언 후 자식객체 생성)으로만 기억하고 있었다.
하지만, 정확한 정의는 "부모 클래스의 규약을 자식클래스가 위반하면 안 된다"이다. 사실 이 정의에 대해서 그렇게 깊게 생각해보지 않았는데 여기에는 오버라이딩을 할 때 주의해야 한다는 의미가 담겨있었다.
그림을 보자

자식이 부모 메서드를 오버라이딩한 상황이다.
여기서 자식은 부모의 규약을 지킨다고 볼 수 있을까? 이렇게 오버라이딩 하게 된다면 부모의 메서드와 자식메서드의 동작 방식이 달라져 직접 메서드의 코드를 보지 않는다면 내부 동작 방식을 알 수 없게 된다는 문제점이 존재하게 된다. (캡슐화가 깨짐)

따라서 이러한 경우에는 이 관계가 꼭 필요한 상속인지 고민해보아야 한다. 이 부분부터는 이렇게 상속해야만 하는 이유와 상속이 굳이 필요 없어 보인다는 두 관점 중 한 가지를 선택해야 한다.
인터페이스와 추상 클래스의 차이
공부를 하며 인터페이스에서도 default 메서드를 선언해 내부에 구현을 작성할 수 있고 private 메서드까지 작성할 수 있다는 사실을 알게 되었다. (private은 default 내부에서 호출)
추상 클래스는 보통 템플릿 메서드 패턴에서 동일하게 중복되는 코드를 구현하고 그 내부 특정 부분에 각 클래스별 조금씩 다르게 동작해야 하는 부분을 abstract 메서드로 선언하고 자식 클래스에게 구현을 강제한다.
따라서 이런 기능적인 부분에서는 인터페이스와 추상클래스의 차이는 크게 없다고 생각된다. 가장 큰 차이라면 추상클래스는 인스턴스 필드를 가질 수 있지만 인터페이스는 상수를 제외하고는 가질 수 없다는 것이 가장 큰 차이라고 생각된다.
물론 인터페이스를 사용할 때와 추상클래스를 사용해야 할 때 사이에 그렇게 겹치는 부분이 없다고 현재는 생각이 들어 그냥 개념적으로 이렇다고 기억해야겠다.
Object 클래스

Object 클래스는 Java의 모든 클래스가 상속하는 클래스로 만약 extends로 아무 클래스도 상속하지 않았다면 default로 Object를 상속받게 된다.
그에 따라, 우리가 흔히 사용하던 메서드들을 사용할 수 있는데 가장 대표적으로 자주 사용되는 메서드는 `equals`, `hashCode`, `toString`이다. toString()은 일반적으로 객체에 대해 로그를 찍을 때 주로 사용한다.
equals는 동등성 비교로 원시 타입(primitive type)이 아닌 참조 타입들에 대해 비교할 때 Heap에서 가리키는 객체가 다르기 때문에 필요한 메서드다. 이때, equals를 통해 객체 안의 값을 비교해 해당 참조 타입이 같은 값인지 아닌지를 비교한다.
Integer a = 12345;
Integer b = 12345;
a == b -> false
a.equals(b) -> true
hashCode는 HashMap에서 equals 메서드와 함께 객체의 값을 비교하는 데 사용된다. 객체의 필드를 해시함수로 해싱한 값을 key로 저장하는데 이때, 자료구조의 Capacity로 해싱을 진행하기 때문에 중복된 key가 나올 수 있다. 이때, equals 메서드를 이용해 동일한 값인지 비교 후 새로운 값을 추가할지 결정한다.
'Java' 카테고리의 다른 글
| 코딩 테스트에서 잘못 사용했던 Object.clone() (0) | 2025.11.04 |
|---|---|
| 디자인 패턴 : 빌더 & 싱글톤 패턴 구현해보기 (with. 이펙티브 자바) (0) | 2025.10.28 |
| 객체지향 실습 - 음식 주문 시스템 구현하기 (1) (0) | 2025.10.14 |
| 객체지향 학습 (1) (0) | 2025.09.09 |