본문 바로가기
Today I Learned

[Java] 스프링 입문 / 학습 노트

by myspace 2025. 2. 26.

 

들어가기에 앞서

해당 포스팅은 인프런의 <스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술> 강의를 듣고 정리한 내용입니다.

인프런 김영한님 무료강의 바로가기

 


 

Java란?

- 객체지향 프로그래밍 언어.

- 플랫폼 독립성이 있다. 한 번 작성해두면 JVM을 통하여 운영체제와 상관 없이 어디서나 실행 가능하다.

- 개발자가 메모리를 직접 할당하고 해제할 필요 없이 Garbage Collection으로 메모리 관리가 자동화 되어있다.

 

OOP (Object-Oriented Programming)란?

- 객체(Object)를 중심으로 프로그램을 설계하고 개발하는 방법론이다.

- 재사용성, 유지보수성, 확장성을 높이는 프로그래밍 패러다임이다.

- 주요 특징으로는 다음 4가지가 있다.

1. 캡슐화 (Encapsulation) : 데이터(필드)와 메서드(함수)를 하나의 클래스 안에 묶어서 외부에서 직접 접근하지 못하도록 한다. 메서드를 통해서만 접근하도록 제한하는 정보 은닉의 특징이 있다.

2. 상속 (Inheritance) : 부모 클래스(상위 클래스)의 필드와 메서드를 자식 클래스(하위 클래스)가 상속받는 것으로, 코드의 재사용성이 증가한다. 단지, Java는 하나의 부모 클래스만 상속 가능하다. 즉, 다중 상속이 불가능하다.

3. 다형성 (Polymorphism) : 같은 메서드 호출이 객체의 타입에 따라 다르게 동작하는 것이다. 메서드 오버로딩, 메서드 오버라이딩이 있다.

  • Java에서 다형성을 사용할 발생할 있는 성능 문제와 대처는?
    • 오버라이딩 된 메서드를 호출할 때 동적 바인딩(Dynamic Binding)으로 인한 성능 저하가 있을 수 있으며, 일반적인 정적 메서드 호출보다 실행 속도가 느려질 수 있으므로 오버라이딩이 불가능한 final 키워드를 사용하여 JVM 최적화를 활성화하면, 정적 바인딩이 가능한 구조로 변경되어 인라인화(Inlining)가 가능하다.
      • 인라인화(Inlining)란 함수를 호출하는 대신, 함수 본문을 그대로 복사해서 실행하는 방식으로 함수 호출에 따른 오버헤드를 줄이고 실행 속도를 향상시키는 방법이다. 단지 동일한 코드가 여러 곳에 위치하므로 메모리 사용량이 증가하고, 캐시 효율이 저하되고, 디버깅이 어려울 수 있다.
    • 객체의 실제 타입을 검사하기 위해 클래스 계층을 확인하는 과정에서 instanceof 연산자를 사용할 때와, 다운캐스팅(Downcasting) 하는 경우 타입 변환 비용이 발생해 성능 저하가 있을 수 있으며, 추상 메서드나 인터페이스를 활용하여 타입 검사 없이 동작하도록 하면 예방 가능하다.
  • 메서드 오버로딩 (Method Overloading) : 같은 이름의 메서드가 매개변수 타입이나 개수에 따라 다르게 동작한다. 즉, 같은 이름의 메서드를 매개변수를 다르게 해서 여러 개 만드는 것이다.
class Calculator {
    // 정수 두 개를 더하는 메서드
    int add(int a, int b) {
        return a + b;
    }

    // 정수 세 개를 더하는 메서드 (오버로딩)
    int add(int a, int b, int c) {
        return a + b + c;
    }

    // 실수를 더하는 메서드 (오버로딩)
    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();

        System.out.println(calc.add(3, 5));       // 8 (int, int)
        System.out.println(calc.add(3, 5, 7));    // 15 (int, int, int)
        System.out.println(calc.add(2.5, 3.2));   // 5.7 (double, double)
    }
}
  • 메서드 오버라이딩 (Method Overriding) : 부모 클래스의 메서드를 자식 클래스에서 재정의한다. 하나의 참조 변수로 여러 타입의 객체를 다루는 동적 바인딩 (Dynamic Binding)으로, 코드 수정 시 부모 클래스만 수정하면 되기에 유지보수 및 확장성이 용이하다.
// 부모 클래스 (Animal)
class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// 자식 클래스 (Dog)
class Dog extends Animal {
    // 부모 클래스의 메서드를 오버라이딩 (재정의)
    @Override
    void makeSound() {
        System.out.println("멍멍!");
    }
}

// 자식 클래스 (Cat)
class Cat extends Animal {
    // 부모 클래스의 메서드를 오버라이딩 (재정의)
    @Override
    void makeSound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        myDog.makeSound();  // "멍멍!" (오버라이딩된 Dog의 메서드 호출)
        myCat.makeSound();  // "야옹!" (오버라이딩된 Cat의 메서드 호출)
    }
}

4. 추상화 (Abstraction) : 추상 클래스 또는 인터페이스를 사용한다. 불필요한 세부사항을 숨기고 중요한 부분만 표현한다.

  • 추상클래스 (Abstract Class) : 공통된 필드(일반 변수)와 메서드를 포함하면서, 일부 메서드는 구현 없이 선언하는 클래스. extends 키워드를 사용해서 단일 상속이 가능하다. 여러 클래스 간 공통 기능을 정의하고 상속을 통해 확장한다. 그러므로 여러 클래스에서 공통된 기능을 제공하면서 일부 기능만 강제하고 싶거나, 생성자가 필요하거나, 단일 상속만 필요할 때 사용한다.
abstract class Animal {
    String name;  // 공통 속성
    int age;

    // 생성자
    Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 공통된 기능 구현 가능
    void eat() {
        System.out.println(name + "이(가) 먹고 있습니다.");
    }

    // 개별적으로 구현해야 하는 기능 (추상 메서드)
    abstract void makeSound();
}

// 하위 클래스에서 추상 메서드 구현
class Dog extends Animal {
    Dog(String name, int age) {
        super(name, age);
    }

    @Override
    void makeSound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    Cat(String name, int age) {
        super(name, age);
    }

    @Override
    void makeSound() {
        System.out.println("야옹!");
    }
}
  • 인터페이스 (Interface) : 모든 메서드가 구현 없이 선언된 클래스. implements 키워드를 사용해서 다중 구현이 가능하다. 클래스 간 공통된 규칙을 강제하고 싶을 때, 다중 구현이 필요할 때, 메서드 구현 없이 설계(규칙)만 정의할 때 사용한다.
interface Sound {
    void makeSound();  // 인터페이스는 메서드의 구현 없이 선언만 가능
}

// 여러 클래스에서 동일한 동작을 정의 가능
class Dog implements Sound {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

class Robot implements Sound {
    @Override
    public void makeSound() {
        System.out.println("기계음: 삐빅!");
    }
}

 

Java Collection Framework란?

- 데이터의 집합을 효율적으로 관리하고 조작하기 위한 자료구조 라이브러리이다.

- 배열의 고정크기라는 단점을 보완한 동적 데이터 저장소로, 삽입, 삭제, 탐색 등의 연산이 편리하다.

- List, Set, Map 등 다양한 자료구조를 지원한다.

  • List : 인덱스 기반의 순서가 있는 데이터의 집합 / 중복 가능 / 정렬 가능 / 배열 기반 (ArrayList) 또는 연결 리스트 (LinkedList)
  • Set : 중복이 없는 데이터의 집합 / 정렬 불가능하지만 LinkedHashSet를 사용하는 경우 삽입 순서 유지 가능 / 해시 기반 (HashSet), 트리 기반 (TreeSet), LinkedHashSet / 데이터 검색, 중복 제거에 유리
  • Map : 키-값 쌍의 데이터 집합 / 키 중복 불가, 값 중복 가능 / 키 기반 정렬 가능 (TreeMap, HashMap, LinkedHashMap)

- 가변 크기 관리와 동적 배열로 인한 메모리 사용 증가로 오버헤드가 발생할 수 있다는 단점이 있다.

  • 구체적인 사례
    • ArrayList의 크기 동적 증가로 인한 오버헤드
      • list.ensureCapacity(1000); 을 사용하여 사전 용량 확보로 해결
      • 요소 개수를 미리 예측하여 ArrayList<>(예상 크기) 설정하여 예방
    • HashMap<Integer, Integer> 사용 시 int → Integer 변환 비용 발생, 즉 Java의 기본 자료형(int, double 등)은 객체가 아니므로, Collection 사용 시 자동 박싱/언박싱 (Boxing/Unboxing)으로 인한 오버헤드
      • 객체 대신 기본 자료형을 지원하는 TIntIntHashMap(Trove) 같은 라이브러리 사용

 


 

프로젝트 환경설정

강의를 위한 세팅

  • java 11 설치 필요하나, 기존 사용하던 java "19.0.1" 버전 유지
// java 버전 확인 (java -version)

java version "19.0.1" 2022-10-18
Java(TM) SE Runtime Environment (build 19.0.1+10-21)
Java HotSpot(TM) 64-Bit Server VM (build 19.0.1+10-21, mixed mode, sharing)

// jdk 버전 확인 (javac -version)

javac 19.0.1
  • h database 1.4.200 설치
  • IDE: IntelliJ 설치
    • 통합 개발 환경(Integrated Development Environment)은 소프트웨어를 개발하는 툴
  • 스프링 프로젝트
    • 예전에는 밑바닥부터 하나하나 만들어야 했지만 요즘에는 스프링 부트를 기반으로 만듦
    • 스프링 부트 스타터 사이트 활용 : https://start.spring.io
      • Gradle / Java / Spring Boot 버전 2.3.1

 

개념 정리

1. 명령어 --version 앞에 붙는 java와 javac의 차이점은 무엇인가?

  java -version javac -version
공통점 JDK(Java Development Kit)에 포함된 명령어
차이점 JVM의 버전을 확인하며, java는 '.class' 파일을 실행시킬 수 있음 Java Compiler (javac)의 버전을 확인하며, javac는 텍스트 파일로 작성된 Java 소스코드 파일을 bytecode로 컴파일해서 '.class' 파일로 저장

 

2-1. Spring 프로젝트에서 Maven과 Gradle의 차이는 무엇인가?

  Maven Gradle
공통점 빌드 (Build) 도구로서 프로젝트의 의존성 관리, 빌드 자동화, 테스트, 배포를 도우며 방식과 성능 차이가 있음
차이점 2004년에 출시되어 과거에 많이 사용 요즘은 2013년에 출시된 Gradle이 더 많이 사용되는 추세
pom.xml에서 의존성 관리 등의 환경 설정 build.gradle, build.gradle.kts에서 의존성 관리 등의 환경 설정
XML 기반으로 구조가 정형화 되어 있어 유연성이 상대적으로 낮으나 학습 쉬움 Groovy나 Kotlin 기반 DSL을 사용한 구조이며, 구조가 정형화 되어있지 않아 유연성 높으나 학습이 어려움
빌드 속도 상대적으로 느림 이미 빌드된 부분을 캐싱하는 Task 기반 캐싱을 지원해 불필요한 재컴파일을 최소화하므로 빌드 속도 빠름

 

2-2. Maven 프로젝트를 STS (Spring Tool Suite) 없이 실행할 수 있나?

다음과 같은 방법들로 가능하다.

1. Maven은 CLI를 지원하므로, 터미널에서 mvn package 후 java -jar target/*.jar를 입력하여 JAR 파일을 실행

2. 순수 Eclipse에서도 프로젝트를 Import한 뒤, Maven Build 실행 (Run As → Maven Build (mvn package 또는 mvn spring-boot:run 실행)

3. VS Code에서 'Maven for Java' 확장 프로그램 사용

4. IntelliJ IDEA에서 pom.xml을 열어 실행

 

2-3. 현재에도 Maven을 사용하는 경우는 이유가 무엇인가?

현재에도 Maven을 사용하는 경우는 너무 대규모 프로젝트라 Gradle로의 이전하기에 비용이 많이 들거나, 정적인 구조로 학습이 쉽거나, 다양한 플러그인과 템플릿이 제공되므로 편하게 사용하고자 할 확률이 높을 것으로 추정한다.

 

2-4. 백엔드의 변화 속도는 프론트엔드에 비해 늦다고 하지만 Maven이 레거시화 된다면 언젠가 Gradle을 대체할 다른 빌드 도구가 또 나올 텐데, Gradle은 이를 방지하기 위하여 어떤 부분을 개발하고 있나?

2025년 3월 6일 기준, 가장 최근 출시된 Gradle Build Tool 8.13에서는 Daemon JVM 자동 프로비저닝을 도입하여 Gradle Daemon에 필요한 JVM을 자동으로 다운로드 하도록 하고 있다. 즉, 프로젝트 빌드(코드 컴파일, 테스트 실행 등)와 Gradle 자체 실행에 필요한 JDK 버전을 프로비저닝하고 선택할 수 있게 됐다. 이제는 로컬에서 아무것도 찾을 수 없는 경우에도 일치하는 Java 툴체인을 다운로드 할 수 있다. 이외에는 플러그인을 적용할 때 확장 프로그램에서 Scala 버전을 명시적으로 선언할 수 있도록 했으며, 테스트 실행 시간을 분석할 때 정확도를 향상하기 위해 밀리초 정밀도의 JUnit XML 타임스탬프를 추가했다.

(출처: https://gradle.org/ )

  • 툴체인 (Toolchain) : 소프트웨어 개발 및 빌드를 자동화하는 데 사용되는 도구들의 집합
    • 코드 작성 : IDE, 텍스트 편집기 ex) IntelliJ, VS Code
    • 컴파일 : 컴파일러 ex) javac
    • 링크/패키징 : 빌드 도구 ex) Maven, Gradle
    • 테스트 : 테스트 프레임워크 ex) JUnit
    • 배포 : CI/CD 툴 ex) Jenkins, GitHub Actions
  • Scala : 객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP)을 동시에 지원하는 JVM 기반 언어로 Java와 완벽히 호환 됨
  • JUnit : Java 애플리케이션의 단위 테스트(Unit Test)를 자동화하는 프레임워크

 

2-5. 의존성 캐싱 (Dependency Caching)은 무엇인가?

의존성 캐싱은 Spring 프로젝트에서 Maven 혹은 Gradle을 사용하여 라이브러리를 다운로드 할 때, 한 번 다운로드 한 라이브러리는 두 번 이상 재다운로드 하지 않고 저장된 것을 사용하는 것이다. 빌드 속도 향상을 위해 사용되는 방식으로 네트워크 비용이 절감 되고, 안정적인 빌드가 가능하고, 의존성 관리를 용이하게 한다.

Maven은 다운로드한 라이브러리를 로컬 저장소(~/.m2/repository/)에 저장한다. Gradle은 로컬 캐시(~/.gradle/caches/)에 저장한다. Gradle은 효율적인 캐싱 기법인 Task 기반 캐싱, 병렬 빌드와 증분 빌드 등을 지원하므로 더 강력한 캐싱 기능을 제공한다.

  • 캐싱 (Caching) : 자주 사용하는 데이터를 미리 저장해서 필요할 때 빠르게 불러오는 기술. 중복 다운로드를 방지하여 속도 개선, 성능 최적화.
  • 태스크 캐싱 (Task Caching) : Task의 이전 실행 결과를 저장하여 재사용.
  • 병렬 빌드 (Parallel Build) : 여러 Task를 동시에 실행하여 속도 향상.
  • 증분 빌드 (Incremental Build) : 변경된 파일만 다시 빌드하여 시간 단축.

의존성 캐싱에 장점이 존재하는 만큼 주의해야 할 점이 있는데, 캐싱된 의존성이 오래 되면 최신 버전과 충돌하여 문제가 발생할 수 있으니 의존성 버전을 명확히 지정하거나 정기적으로 캐시를 무효화하고 의존성을 업데이트 해야 한다. 또한 보한 취약점이 있는 패키지가 캐싱될 경우 보안 패치가 적용되지 않기 때문에, 보안 업데이트가 포함된 의존성을 확인하고 정기적으로 캐시를 무효화한 후 최신 패키지를 적용해야 한다.

 

2-6. 새 프로젝트를 생성한다고 가정할 경우, 의존성 도구를 사용하는 시나리오는 어떻게 되나?

다섯 가지 단계로 표현할 수 있다. 의존성 관리 도구를 사용하면 수동으로 JAR 파일을 다운로드 하지 않고, 필요한 라이브러리를 자동으로 관리할 수 있다.

1. Maven 혹은 Gradle로 프로젝트 생성

2. 의존성 파일 (pom.xml 또는 build.gradle)에 필요한 라이브러리 추가

3. 의존성 설치 (mvn install 또는 gradle build 실행)

4. 애플리케이션 코드 작성

5. 프로젝트 실행 (mvn spring-boot:run 또는 gradle bootRun)