이번 글은 자바를 학습하면서 직접 정리했던 내용을 “흐름이 끊기지 않게” 다듬은 리뷰다.
기본 문법 수준을 넘어서, 객체지향 설계의 이유(캡슐화/상속/다형성), 추상화(추상클래스·인터페이스), 함수형 스타일(Stream/Lambda/Optional), 예외·파일·JSON 처리, 그리고 멀티스레드·네트워킹 관점까지 한 번에 연결해보는 것이 목표다.
클래스는 한마디로 “설계도”다. 동일한 구조의 데이터와 기능을 재사용/확장 가능하게 만들어주며, 큰 시스템일수록 클래스 설계의 품질이 유지보수성과 직결된다.
여기서 가장 먼저 정리해야 하는 포인트가 static이다.
static을 붙이면 “클래스 소속(공유)”이 된다. static을 빼면 “객체 소속(개별)”이 된다. static이 붙으면 객체마다 고유 상태를 갖는 것이 아니라 클래스 단위로 값/로직을 공유하게 된다. 즉, 객체지향 설계의 목적과는 방향이 달라질 수 있다.객체를 생성하기 위한 설계는 결국 “인스턴스 중심”이어야 한다.
static을 제거하면 메서드/변수는 각각 “객체의 메서드/객체의 변수(필드)”가 된다. 그래서 자바는 기본적으로 객체 기반 설계를 권장하고, 이를 객체지향 언어라고 부른다.
객체를 만들기 위해서는 new로 메모리를 할당한다.
즉, 객체는 고유 메모리를 가지고 그 내부의 값을 할당/변경하며 상태를 유지한다.
그렇다면 클래스 메서드(static)는 언제 쓰는가?
객체를 사용하는 이유는 크게 두 가지로 정리할 수 있다.
1) 고유의 메모리: 인스턴스마다 독립적인 상태를 가진다.
2) 데이터의 그룹핑: 관련 데이터를 하나로 묶어 관리가 쉬워진다.
예를 들어 사람을 표현할 때 name, email 등을 사람마다 따로 변수로 만들면 관리가 어려워진다.
Person 클래스를 만들고 객체로 생성하면 “한 사람의 데이터를 하나의 덩어리”로 묶어서 관리할 수 있다.
그리고 실무 관점에서 안정성을 높이려면 필드를 private로 선언하는 것이 거의 표준이다.
private이면 외부에서 p1.name처럼 직접 접근이 불가해진다. 객체 생성 직후 필드 초기값도 알아두면 좋다.
String 등) = null int 등) = 0 같은 기본값값을 세팅하는 방법은 보통 두 가지다.
new)에 필드를 초기화하는 방식으로 초기 세팅에 더 적합하다.그리고 toString()이 없으면 객체 출력 시 기본적으로 클래스명@해시 형태만 출력된다.
오버로딩과 오버라이딩은 이름이 비슷하지만 완전히 다르다.
메서드 오버로딩(Overloading)
println, sum 같은 형태가 대표적이다.메서드 오버라이딩(Overriding)
생성자(Constructor)는 객체가 만들어지는 시점(new)에 자동 호출되는 “초기화용 메서드”다.
생성자의 사용 목적은 명확하다.
setter가 꼭 필요하다면 “의도”를 드러내는 이름을 권장한다.
setBalance(x) 보다는 transfer / deposit / withdraw 처럼 “업무 행위(서비스 의도)”를 담는 방식이 더 좋다.제네릭은 타입을 일반화해서 재사용성을 높이는 문법이다.
List<Account>, List<Post>처럼 도메인 객체를 컬렉션으로 관리하는 경우가 매우 흔하다.실습에서 자주 만나는 포인트는 “타입 선택의 의도”다.
private Long id; null인 것이 더 자연스러운 경우(“아직 부여되지 않음”)가 많아 Wrapper(Long) 사용.long balance; 0인 것이 더 자연스러운 경우가 많아 원시형(long) 사용.staticId 같은 방식은 객체가 생성될 때마다 클래스 내부에서 증가시키는 형태로 구현할 수 있다.
다만 실무에서는 보통 DB의 auto increment(또는 시퀀스 등)로 대체되고, Spring/DB 단계에서 처리되는 경우가 많다.
setter가 필요한 경우도 있다.
setBalance 같은 일반 setter보다 transfer/deposit/withdraw 같은 도메인 행위 메서드가 더 권장된다.객체 메서드 지향 관점도 중요하다.
a.transfer(b, 50000); Account.transfer(a, b, 50000); 서비스를 설계할 때 가장 먼저 해야 할 일은 “핵심 엔티티”를 정의하는 것이다.
관계 표현 예시는 다음과 같다.
Post post = new Post(title, contents, author); 또한 Author에 List<Post> postListByAuthor를 두면 작성자가 작성 글 목록을 “소유/조회”할 수 있는 구조가 된다(양방향 관계 모델링 예시).
List<Post> 때문에 one-to-many 상속은 “기존 클래스를 확장해서 재사용”하는 핵심 도구다.
A 클래스를 extends한 B 클래스는 A의 필드와 메서드를 상속받을 수 있다.상속의 핵심 특징 2가지:
1) 오버라이딩 가능
2) 다형성
A 타입으로 B를 담으면 A에 선언된 메서드만 호출 가능하고, B 고유 메서드는 직접 호출 불가(다운캐스팅 필요).다형성 예시(인터페이스/상속 모두 같은 관점):
Queue<Integer> myQ = new LinkedList<>(); myQ는 Queue 타입이므로 Queue에 정의된 메서드만 사용할 수 있다(LinkedList 고유 기능은 제한).그리고 @Override는 “재정의 의도”를 명시한다.
super는 부모를 가리키는 키워드다.
super() : 부모 클래스 생성자 호출(생성자 첫 줄에서 호출됨) super.필드 / super.메서드() : 부모 멤버에 접근(자식에서 재정의된 메서드와 구분할 때 사용)다형성을 체감하기 좋은 예시:
Animal dog2 = new Dog(); Animal 타입 하나로 여러 자식 객체(Dog, Cat 등)를 받아 공통 로직으로 처리할 수 있다.접근 제어자의 범위는 다음 순서로 넓어진다.
public > protected > default(패키지) > private실무 관례로는 다음 패턴이 일반적이다.
public private 학습/시험에서는 각각의 범위를 정확히 구분해 묻는 경우가 많다.
추상클래스(abstract class)
abstract를 붙여야 한다. extends로 상속받아 추상메서드를 오버라이딩하여 구현해야 한다.인터페이스(interface)
implements로 구현하고 모든 추상메서드를 오버라이딩해야 한다. default/static 메서드로 구현을 포함할 수도 있지만, 학습 단계에서는 “규격” 이해가 핵심이다.상속/구현 형태 요약
extends 추상클래스 implements 인터페이스final 키워드
인터페이스 간에는 extends가 가능하다.
interface A extends B, C 클래스 관련 정리
implements 여러 개 가능, 오버라이딩 구현 강제)익명 클래스/람다는 “이름 없는 구현(일회성 구현)”을 빠르게 만들기 위한 문법이다.
특히 함수형 인터페이스 구현을 간단히 표현할 때 자주 사용된다.
Stream API는 자바의 함수형 프로그래밍 스타일을 지원하는 도구다.
스트림 종류 예시
Stream<T>, IntStream, LongStream 등처리 방식
filter/map/sorted 등(지연 실행, 파이프라인 구성) forEach/collect/count/reduce 등(실행 트리거, 스트림 소모)Optional / OptionalInt 등
max/min 결과가 Optional인 이유: 대상이 비어있으면 “값 자체가 없음”을 표현해야 하므로 0 같은 임의값을 리턴할 수 없기 때문이다. isPresent 등으로 존재 여부를 확인한 뒤 getAsInt/get 등을 사용한다(또는 더 안전한 꺼내기 방식 사용).메서드 참조(Method Reference)
System.out::printlnOptional 객체 처리(값 꺼내기/대안 처리) 대표 방식 4가지
1) isPresent() 확인 + get()/getAsInt()
2) orElse(기본값 제공)
3) orElseGet(기본값을 공급하는 함수 제공)
4) orElseThrow(없으면 예외 발생)
자주 쓰는 패턴은 isPresent 체크 후 get() 또는 orElseThrow로 “없으면 중단” 처리다.
Optional은 직접 생성하는 경우보다, 라이브러리/프레임워크가 Optional로 리턴해주는 값을 “어떻게 처리할지”가 더 중요하다.
예외 관련 개념
throw): 의도된 오류 상황에서 흐름을 중단시키기 위함(더 진행하면 안 되는 상태). try-catch): 중단 자체를 막는 목적도 있지만, 실무에서는 사용자에게 적절한 메시지/응답을 전달하기 위해 처리하는 의미가 크다. Enum(열거형)은 데이터 값을 통일/표준화하기 위해 사용하는 타입이다.
제네릭은 타입을 일반화하여 재사용성을 높인다.
<T>, Object, List<?> 예외가 발생할 수 있는 코드를 try로 감싸고, 실무(웹)에서는 에러 메시지/상태를 HTTP 응답 문서 형태로 만들어 프론트에 전달해야 한다.
프론트는 이를 받아 alert/toast 등으로 사용자에게 안내한다.
예외 강제 발생 → 적절한 예외 처리 흐름을 연습한다.
try-catch 또는 throws 필요) throw new로 발생시키고, 상위에서 처리/전파)IO vs NIO(Non-blocking 관점)
synchronized(동기화)
StringBuffer는 synchronized 기반이라 상대적으로 느릴 수 있다. 실무 관점
JSON은 전체적으로는 문자열이지만 내부적으로는 데이터 타입(boolean, int 등)을 각각 가진다.
ObjectMapper 클래스를 통해 파싱/직렬화를 진행한다.
readValue: 파싱(역직렬화), JSON → 객체 writeValueAsString: 직렬화, 객체 → JSON보통 POST 요청이 들어오면 파싱을 수행하고, GET 요청이 들어오면 직렬화를 수행한다.
이 과정은 스프링에서는 반복이 너무 많아서 자동화되어 있으며, 프론트에서 axios.get으로 받아오면 바로 객체로 쓰는 느낌과 비슷하게 이해할 수 있다.
파싱 시에는 자바의 리플렉션 기술을 통해 객체를 생성한다.
이때 private 설계에도 접근이 가능해지고, 기본생성자와 getter가 JSON 데이터 형식과 동일하게 세팅되어야 하는 이유도 함께 이해할 수 있다.
(이 파트는 제목만 있고 상세가 비어 있으니, 블로그에서는 아래처럼 “다음 학습 예고” 형태로 채우면 자연스럽다.)
자바에서 스레드는 작업을 처리하는 주체(자원)이다.
컴퓨터에서 CPU 코어가 작업을 할당받듯이, 자바 역시 실행 시 main에 스레드가 할당된다.
그리고 웹서비스는 단일 작업이 아니라 여러 요청을 동시에 처리해야 하므로 “스레드/동시성” 개념이 필수로 따라온다.
서버 생성 단계에서 socket을 통해 사용자와 서버 사이의 접점을 만든다(예: 8080이라는 단자를 만든다고 생각).
스프링의 경우 while문이 없어도 서버가 계속 꺼지지 않고 요청을 지속적으로 받는다.
GET 요청에서 데이터를 넘기는 경우도 있는데, 보통 바디에 담기보다는 URL에 함께 담아 넘긴다.
http://localhost:8080/post?title=안녕그리고 순수 자바로 요청/응답 처리에서 어려운 점이 명확하다.
1) 사용자의 요청값 parsing(HTTP request 문서 파싱)
2) 사용자에게 응답값 제공(HTTP response 문서 생성)
이 지점이 바로 스프링(특히 스프링 MVC/스프링부트)이 엄청난 반복 작업을 대신해주는 이유로 이어진다.
DB 연결은 JDBC, MyBatis 등을 사용해 진행한다.
처음에는 “연결/쿼리/결과 매핑”의 흐름을 직접 경험해보는 것이 도움이 되고, 이후 스프링 단계에서 이 반복이 어떻게 추상화/자동화되는지로 연결하면 학습 효율이 좋아진다.