KT-A 10주차-1 / Java

Mini·1일 전

KT-A

목록 보기
14/14

들어가며

미니 프로젝트를 마친 후에는 프론트엔드와 백엔드를 연동하기 위해 Spring 학습할 예정이다.
Spring은 Java를 기반으로 동작하는 프레임워크이기 때문에, Spring을 제대로 이해하기 위해서는 먼저 Java의 기본 문법과 객체지향 프로그래밍 개념을 익혀야 했다. 따라서 Java를 선수 학습한 뒤 Spring을 공부하는 순서로 학습을 진행했다. 이후에는 학습한 내용을 바탕으로 05 미니 프로젝트를 진행하며 실제로 프론트엔드와 백엔드를 연결해 볼 예정이다.
이번 주 정리에서는 학습한 내용이 많아 두 편으로 나누어 작성하려 한다. 이번 글(10주차-1)에서는 Java 학습 내용을 정리하고, 다음 글에서는 Spring(10주차-2) 학습 내용을 다룰 예정이다.
그럼 먼저 Java 학습 내용을 간단히 살펴보자. (IntelliJ IDEA 사용해 진행)


1. Java 언어의 기초

1.1 왜 Java인가?

수업 첫날부터 강사님이 짚어준 부분이다. 국내 백엔드 채용 시장에서 Java/Spring 비율은 약 70~80%다. 전자정부 프레임워크도 Spring 기반이고, 카카오·네이버·배달의민족 등 주요 IT 기업의 백엔드 기술 스택 대부분이 Java + Spring Boot 조합이다.

Python은 AI/ML에 강하지만 대규모 트래픽에서는 Java가 안정적. Java는 타입 안정성 덕분에 컴파일 시점에서 버그를 잡을 수 있다.

1.2 JDK / JRE / JVM 이해하기


Java의 핵심 특징은 WORA(Write Once, Run Anywhere)다. 한 번 작성하면 OS에 상관없이 어디서든 실행된다. 이게 가능한 이유가 JVM이다.

개발자 코드 (.java)
       ↓  javac 컴파일
   바이트코드 (.class)
       ↓  JVM이 각 OS 맞춤형 기계어로 번역
   Windows / Mac / Linux 에서 실행
구분설명
JVM바이트코드를 실행하는 가상 머신
JREJVM + 실행에 필요한 라이브러리 ("실행만 하는 사람")
JDKJRE + 컴파일러(javac) + 디버거 ("개발자는 반드시 JDK")

1.3 변수와 자료형

Java는 정적 타입 언어다. 변수를 선언할 때 타입을 명시해야 한다. Python처럼 x = 10이 아니라 int x = 10;이다.

상황자료형예시
나이, 개수 같은 정수intint age = 20;
키, 점수 같은 실수doubledouble height = 175.5;
성별, 혈액형 (한 글자)charchar grade = 'A';
참/거짓 판단booleanboolean isStudent = true;
이름, 주소, 문장StringString name = "홍길동";

중요한 포인트가 하나 있는데, String은 기본 자료형이 아닌 참조 자료형(Reference Type)이다. 때문에 비교할 때 ==이 아니라 반드시 equals()를 써야 한다.

String a = new String("홍길동");
String b = new String("홍길동");

System.out.println(a == b);       // false → 주소를 비교
System.out.println(a.equals(b));  // true  → 실제 내용을 비교

String은 자주 쓰는 메서드들이 많다.

String name = "Alice";
System.out.println(name.length());          // 5
System.out.println(name.contains("li"));    // true
System.out.println(name.toUpperCase());     // ALICE
System.out.println(name.equals("Alice"));   // true
System.out.println(name.equals("alice"));   // false — 대소문자 구분

형변환(Casting)도 함께 학습했다.

  • 작은 타입 → 큰 타입: 자동 변환 (intdouble)
  • 큰 타입 → 작은 타입: 명시적 변환 필요 ((int) 3.143, 소수점 버려짐)
  • String ↔ 숫자: Integer.parseInt("123"), String.valueOf(123)
int x = 10;
double y = x;                      // 자동 변환: 10.0
int z = (int) 3.14;                // 명시적 변환: 3 (소수점 버려짐)
int n = Integer.parseInt("123");   // String → int: 123
String s = String.valueOf(123);    // int → String: "123"

1.4 Scanner로 사용자 입력 받기

Scannerjava.util 패키지에 있어서 import가 필요하다.

import java.util.Scanner;

Scanner scanner = new Scanner(System.in);
int age = scanner.nextInt();          // 정수 입력
double height = scanner.nextDouble(); // 실수 입력
String line = scanner.nextLine();     // 한 줄 전체 (공백 포함)
String word = scanner.next();         // 단어 하나 (공백 제외)

scanner.close(); // 사용 후 반드시 닫기

nextInt() 이후 nextLine()을 쓰면 버퍼에 남아있는 \n 때문에 입력이 건너뛰어지는 문제가 생긴다. 실습하다가 직접 겪어봤다.

1.5 연산자

전위 vs 후위 증감 연산자:

int a = 5;
int b = ++a;  // a를 먼저 증가시키고 대입 → a=6, b=6

int c = 5;
int d = c++;  // 먼저 대입하고 나서 증가 → c=6, d=5

단락 평가(Short-circuit):

false && (10 / 0 > 5)  // 앞이 false면 뒤는 실행 안 됨 → 오류 발생 안 함
true  || (10 / 0 > 5)  // 앞이 true면 뒤는 실행 안 됨  → 오류 발생 안 함

불필요한 계산을 생략해서 성능을 높이고, 오류도 방지한다.

int year = 2024;
boolean isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
System.out.println(year + "년은 " + (isLeapYear ? "윤년" : "평년")); // 2024년은 윤년

2. 흐름 제어

2.1 조건문 — if와 switch-case

if ~ else if ~ elseswitch ~ case 두 가지 패턴을 다뤘다.

int age = 19;
if (age >= 18) {
    System.out.println("성인");
} else {
    System.out.println("미성년자");
}

// else-if: 연령대 분류
if (age < 13) {
    System.out.println("어린이");
} else if (age < 19) {
    System.out.println("청소년");
} else {
    System.out.println("성인");
}

switch는 하나의 변수를 여러 값과 비교할 때 가독성이 좋다. break를 빠뜨리면 아래 case로 계속 흘러내리는 fall-through 현상이 생기는데, 의도적으로 활용하면 여러 case를 묶을 수 있다.

switch (month) {
    case 3: case 4: case 5:
        System.out.println("봄"); // 3, 4, 5 모두 여기서 처리
        break;
    case 6: case 7: case 8:
        System.out.println("여름");
        break;
    default:
        System.out.println("잘못된 입력");
}

2.2 반복문 — for, while, do-while

세 가지 반복문을 모두 학습했다.

  • for: 반복 횟수를 미리 아는 경우
  • while: 조건이 참인 동안, 횟수가 불확실한 경우
  • do-while: 최소 한 번은 무조건 실행하고 싶을 때

breakcontinue의 차이:

for (int i = 0; i <= 10; i++) {
    if (i == 5) break;     // 반복문 즉시 종료
    if (i == 3) continue;  // 이번 회차만 건너뜀, 반복은 계속
    System.out.println(i);
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
    sum += i;
}
System.out.println("합계: " + sum); // 5050

// while: 9가 입력될 때까지 계속 받기
Scanner scanner = new Scanner(System.in);
int input = 0;
while (input != 9) {
    System.out.print("숫자 입력 (9 = 종료): ");
    input = scanner.nextInt();
}

// continue로 짝수만 더하기
int evenSum = 0;
for (int i = 1; i <= 10; i++) {
    if (i % 2 != 0) continue; // 홀수는 건너뜀
    evenSum += i;
}
System.out.println("짝수 합계: " + evenSum); // 30

2.3 배열

배열은 크기가 고정이다. 선언할 때 크기를 정해야 하고, 반복문과 함께 쓰는 게 기본이다.

int[] scores = new int[5];

for (int i = 0; i < scores.length; i++) {
    scores[i] = scanner.nextInt();
}
int[] scores = {80, 90, 70, 60, 85};
int total = 0;

for (int i = 0; i < scores.length; i++) {
    total += scores[i];
}
double average = (double) total / scores.length;
System.out.println("평균: " + average); // 77.0

scores.length로 크기를 구할 수 있다. 배열은 크기가 고정이라 나중에 배울 ArrayList의 동적 크기 조절이 왜 필요한지 자연스럽게 이해됐다.


3. 메서드와 재사용성

3.1 메서드란?

특정 코드를 모아서 이름을 붙인 집합. 다른 언어의 '함수'와 유사하다. 반복되는 코드를 한 곳에 두고 이름으로 호출할 수 있어 가독성과 유지보수성이 올라간다.

// 반환값이 있는 메서드
public static int add(int a, int b) {
    return a + b;
}

// 반환값이 없는 메서드
public static void printResult(int result) {
    System.out.println("결과: " + result);
}
public static int sub(int a, int b) { return a - b; }
public static int mul(int a, int b) { return a * b; }
public static int div(int a, int b) { return a / b; }

System.out.println(add(17, 5)); // 22
System.out.println(sub(17, 5)); // 12
System.out.println(mul(17, 5)); // 85
System.out.println(div(17, 5)); // 3

3.2 메서드 오버로딩 (Overloading)

같은 이름, 다른 매개변수로 여러 메서드를 정의하는 것. 컴파일러가 호출 시 인자 타입을 보고 어떤 메서드를 쓸지 결정한다.

public static int add(int a, int b) { return a + b; }
public static double add(double a, double b) { return a + b; }

add(10, 20);      // int 버전 호출
add(3.14, 2.71);  // double 버전 호출

매개변수 개수가 달라도 오버로딩이 성립한다.

public static int max(int a, int b) { return a > b ? a : b; }
public static int max(int a, int b, int c) { return max(max(a, b), c); }
public static double max(double a, double b) { return a > b ? a : b; }

max(3, 7);        // int 2개 → 7
max(1, 5, 3);     // int 3개 → 5
max(3.14, 2.71);  // double 2개 → 3.14

반환값만 다르고 매개변수가 같으면 오버로딩이 성립하지 않는다. 이게 처음에 헷갈렸다.


4. 객체지향 프로그래밍 (OOP)

4.1 클래스와 객체

클래스 (Car.java)     ← 설계도, 자료형 역할, 1개
       ↓  new
객체 (avante, sonata) ← 실체, 여러 개 생성 가능

4.2 접근제한자 (Access Modifier)

필드와 메서드에 누가 접근할 수 있는지를 제어하는 키워드다. 4가지가 있다.

접근제한자같은 클래스같은 패키지자식 클래스외부 클래스
public
protected
default (생략)
private

이 강의에서 중점적으로 다룬 건 publicprivate 두 가지다. protected는 상속 파트에서 다시 등장한다.

4.3 캡슐화 (Encapsulation)

필드를 private으로 막고 Getter/Setter를 통해서만 접근하게 하는 패턴. 단순히 접근을 막는 게 목적이 아니라, 객체가 자기 데이터를 스스로 보호하는 게 핵심이다. Setter 안에서 유효성 검사를 넣을 수 있다.

public void setMaxSpeed(int maxSpeed) {
    if (maxSpeed < 0 || maxSpeed > 300) {
        System.out.println("잘못된 속도");
        return;
    }
    this.maxSpeed = maxSpeed;
}

Setter 안에 검증 로직이 있으니, 외부에서는 잘못된 값을 넣어도 객체 내부 상태가 오염되지 않는다.

public void setKorean(int korean) {
    if (korean < 0 || korean > 100) {
        System.out.println("점수는 0 ~ 100 사이여야 합니다.");
        return;
    }
    this.korean = korean;
}

student.setKorean(150); // "점수는 0 ~ 100 사이여야 합니다." 출력, 값 반영 안 됨
student.setKorean(85);  // 정상 저장

4.4 생성자 (Constructor)

객체 생성 시점에 필드를 한 번에 초기화하는 특별한 메서드다. 반환 타입이 없고 클래스 이름과 동일하다.

// 매번 setter를 따로 호출하던 것을
Car a = new Car();
a.setColor("Red");
a.setMaxSpeed(120);

// 생성자 하나로 해결
Car a = new Car("Red", 120, 3);

this 키워드는 "이 객체 자신"을 가리킨다. 매개변수 이름과 필드 이름이 같을 때 구분하는 데 쓴다.

public Car(String color, int maxSpeed) {
    this.color = color;       // this.color → 필드, color → 매개변수
    this.maxSpeed = maxSpeed;
}

생성자도 오버로딩이 된다. 매개변수 개수나 타입이 다르면 여러 생성자를 정의할 수 있다.

public Car() {                          // 기본 생성자 — 기본값으로 초기화
    this.color = "White";
    this.maxSpeed = 100;
}

public Car(String color, int maxSpeed) { // 매개변수 생성자 — 값을 받아 초기화
    this.color = color;
    this.maxSpeed = maxSpeed;
}

4.5 static 키워드

  • static 필드: 모든 객체가 공유하는 하나의 값 (ex. 생성된 객체 수 카운터)
  • static 메서드: 객체 없이 클래스명으로 바로 호출
// 우리가 매일 쓰던 이것들이 전부 static
Integer.parseInt("123");
String.valueOf(100);
Math.max(3, 5);

static 필드는 여러 객체가 공유해야 하는 값을 저장할 때 유용하다.

public class Car {
    private static int count = 0;  // 모든 인스턴스가 공유하는 하나의 변수

    public Car(String color, int maxSpeed) {
        this.color = color;
        this.maxSpeed = maxSpeed;
        count++;                   // 객체가 만들어질 때마다 증가
    }

    public static int getCount() { // 객체 없이 클래스명으로 바로 호출
        return count;
    }
}

Car a = new Car("Red", 200);
Car b = new Car("Blue", 150);
System.out.println(Car.getCount()); // 2

4.6 상속 (Inheritance)

공통 코드를 부모 클래스에 한 번만 작성하고, 자식 클래스들이 가져다 쓰는 구조다.

Vehicle (부모)
  ├── color, maxSpeed, start(), stop()
  │
  ├── Car (자식)   : + trunk, openTrunk()
  └── Truck (자식) : + loadCapacity, loadCargo()

자식 클래스에서 extends로 상속을 선언한다.

super 키워드: this가 "이 객체 자신"을 가리키듯, super는 "부모 객체"를 가리킨다. 자식 생성자에서 부모 생성자를 호출할 때 첫 줄에서 반드시 사용한다.

public class Car extends Vehicle {
    private int trunk;

    public Car(String color, int maxSpeed, int trunk) {
        super(color, maxSpeed); // 부모 생성자 호출 — 반드시 첫 줄
        this.trunk = trunk;
    }
}

protected 접근제한자: 부모 클래스의 필드를 private으로 선언하면 자식 클래스에서도 직접 접근이 불가능하다. protected로 선언하면 자식 클래스에서는 직접 접근이 가능하면서, 외부 클래스에서는 막힌다.

public class Vehicle {
    protected String color;   // 자식 클래스에서 직접 접근 가능
    private int maxSpeed;     // 자식 클래스에서도 직접 접근 불가
}

public class Car extends Vehicle {
    public void show() {
        System.out.println(color);     // OK — protected
        System.out.println(maxSpeed);  // 컴파일 에러 — private
    }
}

4.7 메서드 오버라이딩 vs 오버로딩

표로 정리!

구분오버로딩 (Overloading)오버라이딩 (Overriding)
위치한 클래스 안에서부모-자식 관계에서만
조건같은 이름, 다른 매개변수같은 시그니처, 다른 동작
목적같은 기능의 다양한 타입 처리자식 클래스에서 동작 재정의
// 오버라이딩: @Override 어노테이션으로 명시
@Override
public void move() {
    System.out.println("도로를 달린다"); // 부모의 "이동한다"를 재정의
}

4.8 다형성 (Polymorphism)

부모 타입 변수에 자식 객체를 담을 수 있다(업캐스팅). 같은 메서드 호출이 실제 객체에 따라 다르게 동작한다.

Vehicle[] vehicles = {
    new Car("Red", 200, 500),
    new Truck("White", 100, 5000),
    new Car("Black", 180, 400)
};

for (Vehicle v : vehicles) {
    v.move(); // Car면 "도로를 달린다", Truck이면 "화물을 싣고 달린다"
}

새 자식 클래스가 추가되어도 for 문 코드는 수정 불필요. 유지보수성과 확장성의 핵심이다.

instanceof + 다운캐스팅: 업캐스팅된 변수로는 자식 고유의 메서드를 호출할 수 없다. 실제 타입을 확인한 뒤 자식 타입으로 명시적으로 변환(다운캐스팅)해야 한다.

Vehicle v = new Car("Red", 200, 500);

v.move();        // OK — Vehicle에 있는 메서드
v.openTrunk();   // 컴파일 에러 — Vehicle에는 없음

// instanceof로 실제 타입 확인 후 다운캐스팅
if (v instanceof Car) {
    Car c = (Car) v;   // 다운캐스팅
    c.openTrunk();     // OK
}

배열에서 자식별로 다른 처리가 필요할 때 자주 쓰이는 패턴이다.

for (Vehicle v : vehicles) {
    v.move();  // 공통 동작
    if (v instanceof Car) {
        ((Car) v).openTrunk();
    } else if (v instanceof Truck) {
        ((Truck) v).loadCargo();
    }
}

5. 인터페이스와 설계 패턴

5.1 인터페이스란?

Java는 단일 상속만 지원한다. 그런데 한 객체가 여러 "역할"을 동시에 가져야 하는 경우가 많다. Car는 "탈것"이면서 동시에 "팔 수 있는 상품"이기도 하다.

상속 (extends)        → "이런 종류의 것이다" (한 개만 가능)
인터페이스 (implements) → "이런 일을 할 수 있다" (여러 개 가능)
public class Car extends Vehicle implements Sellable, Insurable {
    // 종류는 Vehicle 하나
    // 역할은 Sellable + Insurable 둘 다
}

인터페이스는 메서드 시그니처만 정의하고 구현하지 않는다. 구현체(클래스)가 implements로 받아 실제 동작을 채워 넣는다.

5.2 의존성 주입(DI) 패턴

강한 결합의 문제:

// OrderService 안에 KakaoPay를 직접 생성하면
// 결제 수단을 바꾸려면 OrderService 코드를 직접 수정해야 함
private KakaoPay payment = new KakaoPay();

인터페이스 기반 느슨한 결합:

public class OrderService {
    private final PaymentMethod payment; // 인터페이스 타입

    public OrderService(PaymentMethod payment) {
        this.payment = payment; // 외부에서 주입
    }
}

// 구현체를 자유롭게 교체 가능, OrderService 코드 수정 0
new OrderService(new KakaoPay());
new OrderService(new NaverPay());
new OrderService(new CreditCard());

Spring Boot의 @Autowired가 이 "외부에서 주입"하는 과정을 자동으로 해준다. 지금 손으로 하는 걸 Spring이 대신해주는 것이다.

이 개념을 이해하고 Spring때 사용하자


6. 예외처리

6.1 런타임 에러를 우아하게 처리하기

컴파일 에러는 IDE가 미리 잡아주지만, 런타임 에러는 실행 중에 갑자기 터진다.

try {
    int num = Integer.parseInt("abc"); // 여기서 예외 발생
    System.out.println("성공: " + num); // 실행 안 됨

} catch (NumberFormatException e) {
    System.out.println("실패: " + e.getMessage()); // 여기로 이동

} finally {
    System.out.println("항상 실행"); // 예외 여부와 무관하게 항상
}

6.2 다중 catch

try 블록에서 여러 종류의 예외가 발생할 수 있을 때 catch를 여러 개 작성한다. 순서가 중요한데, 자식 예외를 부모 예외보다 먼저 작성해야 한다. 부모가 먼저 오면 자식 예외까지 모두 잡아버려 아래 catch가 실행될 일이 없어진다.

try {
    int idx = Integer.parseInt(idxInput);   // NumberFormatException 가능
    int num = data[idx];                    // ArrayIndexOutOfBoundsException 가능

} catch (NumberFormatException e) {
    System.out.println("숫자 변환 실패");

} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 범위 초과");

} catch (Exception e) {         // 부모 예외는 가장 마지막에
    System.out.println("기타 예외");
}

6.3 throw vs throws

헷갈리기 쉬운 두 키워드다.

키워드위치역할
throw메서드 내부예외 객체를 직접 던진다
throws메서드 시그니처이 메서드가 예외를 던질 수 있다고 선언
// throw — 조건에 맞으면 예외를 직접 발생
public void setMaxSpeed(int maxSpeed) {
    if (maxSpeed < 0) {
        throw new IllegalArgumentException("음수 불가: " + maxSpeed);
    }
    this.maxSpeed = maxSpeed;
}

// throws — 체크 예외를 호출자에게 위임 (RuntimeException은 생략 가능)
public void readFile(String path) throws IOException {
    // 파일 읽기 코드
}

Spring Boot는 RuntimeException 위주라 throws 선언을 거의 쓰지 않는다. 메서드 시그니처가 깔끔하게 유지된다.

6.4 체크 예외 vs 언체크 예외

구분처리 강제예시
체크 예외필수 (try-catch 또는 throws)IOException, SQLException
언체크 예외 (RuntimeException)선택NumberFormatException, NullPointerException

Spring Boot는 언체크 예외를 선호한다. 비즈니스 로직 코드가 try-catch로 도배되는 걸 방지할 수 있어서다.

6.5 사용자 정의 예외

RuntimeException을 상속해서 의미 있는 이름의 예외를 직접 만든다.

public class PaymentFailedException extends RuntimeException {
    public PaymentFailedException(String reason) {
        super("결제 실패: " + reason);
    }
}

IllegalArgumentException 대신 PaymentFailedException을 쓰면 예외 이름만 봐도 어떤 상황인지 즉시 파악된다. Spring Boot의 UserNotFoundException, OrderNotFoundException 같은 패턴이 전부 이것이다.


7. 컬렉션과 람다

7.1 배열의 한계와 List

배열은 크기가 고정이다. List는 크기가 동적으로 늘어나고 편리한 메서드를 제공한다.

List<String> names = new ArrayList<>();
names.add("홍길동");
names.add("이영희");

names.get(0);            // 조회
names.size();            // 크기
names.remove(0);         // 삭제
names.contains("이영희"); // 포함 여부

7.2 Map — 키-값 쌍으로 저장

Map<String, Integer> scores = new HashMap<>();
scores.put("홍길동", 85);
scores.get("홍길동"); // 85

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

List vs Map 선택 기준:

  • 순서가 중요하고 인덱스로 접근 → List
  • 이름표(키)로 빠르게 찾고 싶음 → Map

7.3 람다 표현식

메서드를 한 줄로 표현하는 문법이다.

// 전통적인 for + if 방식
List<Integer> highScores = new ArrayList<>();
for (int score : scores) {
    if (score >= 70) highScores.add(score);
}

// 람다 + Stream 방식
List<Integer> highScores = scores.stream()
    .filter(score -> score >= 70)
    .collect(Collectors.toList());

7.4 Stream API

컬렉션 데이터를 흐름처럼 처리하는 API다. 3단계 파이프라인으로 동작한다.

stream()          → 컬렉션을 스트림으로 변환
↓
.filter(조건)      → 조건 맞는 것만 통과
.map(변환)         → 각 원소를 다른 형태로 변환
↓
.collect(...)     → List 등으로 수집
// 70점 이상만 필터링하고, 각 점수를 등급 문자열로 변환
List<String> grades = scores.stream()
    .filter(score -> score >= 70)
    .map(score -> score + "점 (Pass)")
    .collect(Collectors.toList());

7.5 Optional — null을 안전하게 다루기

Optional<Integer> first = list.stream().findFirst();

first.ifPresent(n -> System.out.println("첫 원소: " + n)); // 값이 있을 때만
int n = first.orElse(0);                                    // 없으면 기본값
int n = first.orElseThrow(() -> new RuntimeException("없음")); // 없으면 예외

null 체크를 흩뿌리는 대신 Optional로 감싸면, "값이 없을 수도 있다"는 사실이 타입에 드러난다. Spring Data JPA의 findById()Optional<T>를 반환하는 이유가 바로 이것이다.

// 지금 배운 것들이 Spring Boot 서비스 코드로 그대로 이어진다
public List<UserDto> findActiveUsers() {
    return userRepository.findAll()
        .stream()
        .filter(u -> u.isActive())
        .map(u -> new UserDto(u.getName()))
        .collect(Collectors.toList());
}

public User findById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id)); // 6장 사용자 정의 예외
}

생각정리

학부 과정에서 Java와 Spring을 모두 접해본 경험이 있었기 때문에 이번 학습에서 완전히 새로운 내용을 배우는 느낌은 아니었다. 오히려 예전에 배웠던 개념들을 다시 정리하고, Spring Boot를 사용하기 전에 필요한 Java 기초를 복습하는 시간에 가까웠다. 특히 객체지향 프로그래밍 파트는 예전에도 여러 번 공부했지만, 다시 학습하면서 클래스, 상속, 다형성, 인터페이스가 각각 왜 필요한지 조금 더 명확하게 이해할 수 있었다. 학부 시절에는 문법을 외우는 데 집중했다면, 이번에는 실제 프로젝트를 진행한다는 목적이 있어서 각 개념이 어떤 문제를 해결하기 위해 존재하는지 생각하며 학습하게 되었다.
가장 인상 깊었던 부분은 인터페이스와 의존성 주입 개념이었다. Spring을 사용할 때는 @Service, @Repository, @Autowired 등을 자연스럽게 사용했지만, 이번에는 그 동작 원리가 되는 구조를 Java 코드만으로 직접 구현해보며 다시 정리할 수 있었다. 덕분에 Spring이 단순히 편리한 프레임워크가 아니라 객체지향 설계 원칙을 기반으로 여러 기능을 자동화해주는 도구구나.. 알고 잘 사용하자 라는 느낌을 받은거 같다.
컬렉션, 람다, Stream API, Optional과 같은 기능들은 실제 백엔드 개발 과정에서 자주 사용되는 만큼 활용 방법을 다시 들으며 찾아보고 공부하게 되었다. 특히 Stream API는 데이터를 가공하는 코드를 간결하게 작성할 수 있어 앞으로 진행할 프로젝트에서도 적극적으로 활용할 수 있을 것 같다.
다음 주(11주차)에는 Spring Boot 학습을 통해 이번에 정리한 객체지향 개념과 설계 방식을 생각해보자.
(예비군 훈련이 6/8 ~ 6/11이라 참여는 불가능할거 같지만.. 후에 따로 작업해보자)
10주차-1을 마무리한다.


참고/출처

실습 Code

profile
發現(발현)

0개의 댓글