728x90

들어가며


스프링 프레임워크 기본편을 수강하고서, 간단한 웹 어플리케이션을 만들기 위해 스프링 부트로 넘어오게 되었다. 아직 MVC패턴, DB연동기술 그리고 고급 활용에 대해 공부할게 많지만 간단하게나마 웹 페이지를 구축하고 나면 이후 학습에 도움이 되리라고 생각한다!

프로젝트 설정


핵심 라이브러리
스프링 MVC
스프링 ORM
JPA, 하이버네이트
스프링 데이터 JPA
기타 라이브러리
H2 데이터베이스 클라이언트
커넥션 풀: 부트 기본은 HikariCP
WEB(thymeleaf)
로깅 SLF4J & LogBack
테스트

Spring Initializr

위 페이지에서 매우 간단하게 스프링 부트 프로젝트를 세팅할 수 있다.

그리고 프로젝트의 SDK와 Gradle JVM의 버전을 꼭 확인해야한다.

도메인 설계


엔티티와 테이블 분석


가장 먼저 중심부를 보면 ORDERS와 ITEM이 놓여있다. 이로써 상품과 주문을 중심으로 비즈니스 로직이 돌아가는 프로젝트임을 알 수 있다.

 

1. 주문 클래스 위아래로 회원 클래스와 배송 클래스가 붙어 있음을 볼 수 있다.

- 회원 클래스는 일대다로 회원을 기준으로 여러 주문을 가질 수 있다.
- 배송 클래스는 주문과 일대일 관계로 하나의 주문은 하나의 배송을 가진다.

 

2. 상품 클래스는 카테고리와 다대다 관계를 가진다.

그러나, 실제로 직접 다대다 연결을 할 수 없어 중간에 카테고리_상품 클래스를 두고 각각 다대일 관계를 맺어 연결한다.

 

3. 테이블 설계와 연관관계 매핑의 차이:

참조와 매핑이라는 단어를 생각해보면 이해가 쉽다.
공통적으로는 정보덩어리인 엔티티를 연결시킨다.
테이블 설계는 데이터베이스에서 테이블 간의 참조를 통한 관계 형성을 다루고, 연관관계 매핑은 객체간의 관계를 설정한다.
쉽게 테이블 참조는 비교적 간단한 관계 형성에 사용된다. 이를테면 회원과 주문이 있을 때, 이 주문이 어떤 회원의 것인지 알기 위해 회원의 고유키를 주문의 외래키로 참조하는 것이다.
반면, 객체의 관계에서는 좀더 복잡해진다. 주문과 배송은 각각 서로를 위해 존재한다. 하나의 주문에는 하나의 배송이 있어야 하고 하나의 배송에는 하나의 주문 정보가 필요하다. 반대로 회원과 주문처럼 한명의 회원이 여러 주문을 가지는 관계도 있다. 

 

 

728x90

들어가며


인텔리J와 스프링 부트를 사용하는 요즘에는 톰캣이 내장되어 따로 설치할 필요가 없지만, 설치부터 배포까지 한번 해보면 톰캣 서버의 동작에 대해 이해가 쉽다고 하여 톰캣 관련한 글을 작성했습니다.

톰캣 설치


Apache Tomcat® - Apache Tomcat 10 Software Downloads

 

Apache Tomcat® - Apache Tomcat 10 Software Downloads

Welcome to the Apache Tomcat® 10.x software download page. This page provides download links for obtaining the latest version of Tomcat 10.1.x software, as well as links to the archives of older releases. Unsure which version you need? Specification versi

tomcat.apache.org

 

1. 위 링크로 들어가서 zip 폴더를 다운받아 압축을 해제한다.

2. 명령프롬프트에서 cd명령어를 통해 폴더 경로로 들어간다.

 

C:\Users\***  cd C:\Users\***\Downloads\apache-tomcat-10.1.16\apache-tomcat-10.1.16
C:\Users\***\Downloads\apache-tomcat-10.1.16\apache-tomcat-10.1.16>cd bin

3. startup.bat을 누르면 톰캣이 활성화된다.

4. localhost:8080 에 들어갔을때 아래와 같이 페이지가 나오면 성공!

Neither the JAVA_HOME nor the JRE_HOME environment variable is defined At least one of these environment variable is needed to run this program

 

이런 문구와 함께 동작하지 않는다면 JAVA_HOME 환경변수가 설치되지 않았을 확률이 높으므로, 아래 더보기를 통해 환경 변수 설정을 해보는 것이 좋다.

더보기

Windows에서 JAVA_HOME 환경 변수 설정:

  1. 자바 설치 경로 확인: 자바가 설치된 경로를 확인합니다. 예를 들어, Java가 C:\Program Files\Java\jdk-17에 설치되어 있다면 해당 경로를 확인합니다.
  2. 시스템 환경 변수 설정:
    • 시작 메뉴에서 마우스 오른쪽 클릭 후 시스템을 선택합니다.
    • 왼쪽 메뉴에서 고급 시스템 설정을 클릭합니다.
    • 환경 변수 버튼을 클릭합니다.
  3. 새로운 시스템 변수 추가:
    • 새로 만들기(N)를 클릭합니다.
    • 변수 이름: JAVA_HOME
    • 변수 값: Java가 설치된 디렉토리 경로 (예: C:\Program Files\Java\jdk-17)
    • 확인을 클릭하여 변수를 추가합니다.
  4. PATH에 JAVA_HOME 추가:
    • 시스템 변수 목록에서 Path를 선택하고 편집(E)을 클릭합니다.
    • 새로 만들기(N)를 클릭하고 %JAVA_HOME%\bin을 추가합니다.
    • 확인을 클릭하여 변경 사항을 저장합니다.
  5. 변경 사항 적용:
    • 환경 변수 설정 창에서 확인 버튼을 클릭합니다.
    • 변경 사항이 적용되려면 명령 프롬프트를 다시 시작하거나 시스템을 재부팅합니다.
  6. 재시작 후 확인:
    • 명령 프롬프트에서 java -version 명령어를 실행하여 Java 버전이 표시되는지 확인합니다.
  7. Tomcat 실행:
    • Tomcat의 startup.bat을 실행해 봅니다.

빌드와 배포


html을 작성하였다면 빌드와 배포를 통해 톰캣 서버를 이용하여 화면에 띄울 수 있다.

명령 프롬프트에  gradlew build 를 입력하면 war 파일이 생성된다.

 

war 압축을 아래 명령어를 통해 풀면 프로젝트에서 만든 파일들을 볼 수 있다.

jar -xvf server-0.0.1-SNAPSHOT.war

다시 war파일을 톰캣이 저장된 폴더 내에 webapps라는 폴더 안에 파일명을 ROOT로 변경하여 저장

그리고 startup.bat으로 톰캣을 실행하면 localhost:8080에는 내가 작성한 html 화면이 나타난다.

 

 

 

'개발 환경' 카테고리의 다른 글

프로젝트 관리를 위해 Github Repo에 IDE연동하기  (0) 2023.12.16
728x90

들어가며


5편에서는 IoC컨테이너(스프링 컨테이너)를 통해 Config창을 따로 두어 역할 분리를 했습니다.

또한, @Configuration과 @Bean을 통해 빈 저장소를 만들고 빈을 생성했습니다.

그러나 Bean을 각각 지정해야하는 불편함이 있음을 알았고 이를 해결하기 위한 방법을 공부하려고 합니다.

컴포넌트 스캔


 

@ComponentScan(스캐너), @Component(바코드)

업무를 하다보면 비즈니스 로직을 구성한 뒤에 따로 설정 정보를 입력하기가 매우 귀찮다. 그래서 비즈니스 로직 창에 @Component을 달면 설정창의 ComponentScan이 알아서 찾아 등록해준다.

 

그렇지만 장점에는 단점도 항상 존재한다.

편리하고 유연한 대신에 설정창에서 명시적으로 빈의 역할을 확인할 수 있던 것과 달리 직접 확인이 어렵다. 또한, 대규모 프로젝트에서 오버헤드가 발생하기도 한다.

더보기

컴포넌트 스캔(Component Scanning)은 스프링이 자동으로 빈(Bean)을 찾아 등록하는 메커니즘 중 하나입니다. 스프링은 어플리케이션을 실행할 때, 특정 패키지 이하에서 @Component 및 관련 어노테이션들이 붙은 클래스를 찾아서 자동으로 빈으로 등록합니다.

 @ComponentScan 어노테이션을 사용하면 @Component 외에도 @Controller, @Service, @Repository, @Configuration 등과 같은 특정 어노테이션이 붙은 클래스들도 스캔 대상에 포함된다.

package hello.core;
import ...

@Configuration
@ComponentScan
	public class AutoAppConfig {
}
@Component
public class OrderServiceImpl implements OrderService {
 private final MemberRepository memberRepository;
 private final DiscountPolicy discountPolicy;
 @Autowired
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
 }
}

@ComponentScan, @Component를 통해 설정 정보 작성, 그리고 @Autowired를 통해 의존관계 주입.

탐색 위치 지정과 필터


위에서 말했듯, @ComponentScan을 사용시, 자동으로 등록해 대규모 프로젝트의 경우 오버헤드의 위험이 있다. 이 단점을 해결하기 위한 방법이 탐색 위치 지정 그리고 필터다.

 

탐색 위치 지정

basePackages를 통해 시작 위치를 설정하는 방법이 있다.

그러나 추천하는 방식은 프로젝트의 상단을 활용해 시작점을 두는 것이다.

@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // configuration content here
}

필터

@Component, @Service, @Repository, @Controller 등 컴포넌트 등록할 대상과 제외할 대상을 구별할 수 있다.

 

 

롬복 lombok


컴포넌트 스캔을 통해 설정창을 간단하게 정리하고 비즈니스 로직을 구성하는 창에는 @Component와 @Autowired를 통해 자동으로 등록할 수 있게 하고 의존관계를 정리했다.

 

그리고 lombok을 이용하면 비즈니스 로직을 보다 간단하게 정리할 수 있다.

아래 코드를 보면,

OrderServiceImpl가 주문서비스를 의존해 생성자를 생성하기 위해 초기화하고 있다.

private final을 통해 불변하게 멤버저장소와 할인정책을 의존 가능하게 주입받고 있다.

이외에 생성과 관련된 코드는 @RequiredArgsConstructor을 통해 자동으로 진행된다.

더보기

롬복은 주로 클래스의 Getter, Setter, Equals 및 HashCode 메서드, ToString 메서드 등을 애노테이션을 사용하여 생성할 수 있게 해줍니다. 이로써 개발자는 이러한 반복적인 코드를 일일이 작성하지 않아도 됩니다. 여러 롬복 애노테이션 중에서 @Getter, @Setter, @ToString, @EqualsAndHashCode 등은 주로 빈 클래스에서 사용되며, 이는 주로 스프링의 컴포넌트 스캔과 함께 사용됩니다.

@Component
@RequiredArgsConstructor
	public class OrderServiceImpl implements OrderService { 
		private final MemberRepository memberRepository;
		private final DiscountPolicy discountPolicy;
}

 

728x90

들어가며


지난 4편에서 좋은 객체 지향 프로그래밍을 위해 Config 구성 인터페이스를 만들어 역할을 분리하는 것에 대해 공부했습니다. 이번 5편에서 다룰 스프링 컨테이너 즉, IoC 컨테이너는 이렇게 따로 분리해 낸 Config 인터페이스를 @ 어노테이션을 활용해 보다 더 모듈화하고 유지보수에 용이하도록 로직을 다듬으려고 합니다.

스프링 컨테이너란


 

스프링 컨테이너란?

스프링 컨테이너는 스프링 프레임워크에서 IoC (Inversion of Control)를 구현한 핵심 컴포넌트다. IoC란 객체의 생성, 관리, 생명주기를 개발자가 아닌 프레임워크 또는 컨테이너가 담당하는 디자인 패턴을 말한다.

 

위 그림과 같이 IoC컨테이너를 이용하면 AnnotationConfigApplicationContext를 읽어 대신 객체를 생성하고 의존관계를 주입해준다는 것인데, 이것은 자바 코드를 통해 직접 구현할 수 있었다.

그럼에도 스프링 컨테이너를 사용하는 이유는 아래에서 설명할 싱글톤 컨테이너의 개념과 연관된다.

//MemberApp 내용 수정!

//AppConfig를 통해 직접 객체를 생성하고 의존관계를 주입했었다.
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();

//스프링 컨테이너를 이용해 빈 저장소로부터 빈을 관리한다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

 

 

스프링 컨테이너를 사용해야 하는 이유 = 싱글톤 컨테이너


1. 스프링 없는 순수한 DI컨테이너

다시 등장한 자동차 그림이다. 이미 좋은 객체 지향 프로그래밍을 위해 역할 분리를 마친 상태다.

운전자는 K3를 탈지, 아반떼를 탈지, 모델3를 탈지 모르고 자동차가 필요해 호출한다.

그러면 config 구성 창에서 지정된 K3 객체를 new를 통해 생성하게 되는 것이다.

그런데 운전자가 만약 자동차를 한번 더 호출한다고 하자. 이 상황에서 new를 통해 또다른 K3가 생성된다.

결국 운전자가 필요한 자동차는 1대인데 수십 수백대가 생성된다면, 엄청난 자원 낭비인 것이다.

2. 싱글톤 패턴을 적용

아래는 싱글톤 패턴을 위해 임의로 만든 서비스다.

싱글톤 패턴은 new를 통해 객체가 무한정 생성되는 상황을 방지하기 위해 객체가 생성되고 나면 private을 통해 추가 생성을 방지하는 디자인 패턴이다.

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }
    private SingletonService() {
    }

 

즉, K3 객체 1대만 생성해서 Config 구성창에서 K3를 운전하도록 지정된 운전자들은 모두 공유하도록 하는 것이다. 렌터카(?)를 생각하면 비슷할지도...

공유되는 인스턴스다 보니 여러가지 문제점이 등장한다.

  1. .getInstance를 통해 직접 구현체를 의존해 앞서 강조했던 DIP, OCP를 위반한다.
  2. Stateful이 아닌 Stateless 무상태로 설계해야 한다.

Stateful과 Stateless


Stateful은 자차, Stateless는 렌터카라고 비유해보면, km당 이용 요금을 내야 한다고 하자.
Stateful은 자차이기 때문에 다른 운전자가 이용하지 않아 계기판을 확인하면 바로 내가 탄 키로수를 확인할 수 있다. 그러나 Stateless는 내가 이용하고 바로 확인하지 않고 그 사이에 다른 운전자가 이용하게 된다면, 계기판에는 내가 탄 키로수가 아닌 다른 운전자가 탄 키로수가 적혀 있을 것이다.

자원의 효율 측면에서는 장점을 가지고 있지만 변경과 저장이 어렵다는 점에서 단점을 지니고 있다.

 

*아래는 간단한 스레드를 통한 예시

//ThreadA: A사용자 1000키로 이용
 statefulService1.distance("userA", 1000);
 //ThreadB: B사용자 2000키로 이용
 statefulService2.distance("userB", 2000);
 //ThreadA: 사용자A 키로수 조회
 int kilometer = statefulService1.getKilometer();
 //ThreadA: 사용자A는 1000키로를 이용했지만, 2000키로 출력
 System.out.println("kilometer = " + kilometer);

3. 스프링 컨테이너와 싱글톤

스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다
른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한다.

 

스프링 컨테이너에서는 이런 논리로 싱글톤을 유지한다고 하는데,, 이 부분은 깊게 파기보다 @Configuration을 사용하면 된다는 정도면 충분하다.

@Configuration, @Bean


@Configuration 애노테이션을 통해 빈 저장소를 만들고, @Bean 애노테이션을 부여해 빈 저장소에 저장한다.

저장된 Bean은 .getBean을 통해 조회 할 수 있다.

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장한다.

package hello.core;

import ...

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

 

 

@Test 코드를 이용한 컨테이너 테스트

package hello.core.singletone;

import ...

public class SingleToneTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();
        //조회 : 호출할 때 마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonService() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("memberService1 = " + singletonService1);
        System.out.println("memberService2 = " + singletonService2);
    }

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void SpringContainer() {

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        //조회 : 호출할 때 마다 객체를 생성
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }
}

 

728x90

좋은 객체 지향 프로그래밍 SOLID


용어 개념
SRP 단일 책임 원칙 = 하나의 클래스는 하나의 책임을 가짐
OCP 개방 및 폐쇄 원칙 = 확장에는 열려 있으나, 변경에는 닫힘
LSP 리스코프 치환 원칙 = 객체는 프로그램의 정확성을 지켜야
ISP 인터페이스 분리 원칙 = 여러 개의 인터페이스가 범용 하나보다 우수함 
DIP 의존관계 역전 원칙 = 추상화에 의존, 구체화에 의존 x

 

실무에서는 비즈니스 요구사항이 굉장히 자주 바뀌기 때문에 OCP와 DIP를 준수하는 것이 변경사항에 대비하기 유리하다고 한다. 

* 아래 코드는OCP와 DIP를 위반한 예제 코드다. 

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
 private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

 

실제 주문서비스를 기능하기 위한 OrderServiceImpl(구현체)는 OrderService(인터페이스)를 상속받아 생성된다.

이때, 할인 정책을 도입하기 위해 DIscountPolicy(인터페이스)에 의존관계를 가지며 private final을 통해 외부에서 고정된 값으로 실제 할인정책(구현체)를 사용한다.

-> //주석 처리를 통해 Fix에서 Rate 정책으로 변경하려는 코드임을 알 수 있다.

OCP와 DIP 위반


2편에서 객체에 대해 설명하며 사용했던 그림이다.

 

JAVA는 객체 지향 프로그래밍? Jav Spring 프레임워크의 기본 (2)

들어가며 안녕하세요, Spring 프레임워크와 관련해 시리즈로 글을 쓰기로 마음을 먹었는데,, 사실 인프런의 김영한 선생님 커리큘럼을 수강중이라 수강 후기에 가까울 것 같습니다. ㅎㅎ 이전 글

wooltech.tistory.com

OrderServiceImpl을 운전자라고 치자.

DiscountPolicy가 자동차 역할, FixDiscountPolicy와 RateDiscountPolicy는 K3, 아반떼, 모델3라고 할 수 있다.

 

운전자는 어딘가로 이동하겠다는 목적을 위해 자동차를 이용하고자 한다. 이때 자동차를 타고 이동하는 것이 중요한 것이지 어떤 기종을 타는지는 목적을 이루는데 중요하지 않다.

그러나, 위 예제 코드에서는 K3를 타도록 지정되어 있는 것과 같다. - DIP 위반!

 

그리고 외부 사정으로 인해 K3를 이용하기 어려워 아반떼로 변경되는 상황에서 운전자에게 변경 정보를 입력해 아반떼를 찾아 타게 만들어야 하는 것이다. - OCP위반 !

Config 구성 클래스


운전자가 자동차 운전(실행)에만 집중할 수 있도록 자동차 설정은 Config라는 구성클래스를 통해 따로 관리해야한다.

package hello.core.order;
import ...

public class OrderServiceImpl implements OrderService {
	private final MemberRepository memberRepository;
	private final DiscountPolicy discountPolicy;
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
 		this.memberRepository = memberRepository;
 		this.discountPolicy = discountPolicy;
 	}
 	@Override
	public Order createOrder(Long memberId, String itemName, int itemPrice) {
		Member member = memberRepository.findById(memberId);
 		int discountPrice = discountPolicy.discount(member, itemPrice);
 		return new Order(memberId, itemName, itemPrice, discountPrice);
 	}
}

 

package hello.core;
import ...

public class AppConfig {
	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
	}
 	public OrderService orderService() {
 	return new OrderServiceImpl(
 		new MemoryMemberRepository(),
 		new FixDiscountPolicy());
 	}
}
  • 위 코드는 OrderService메서드에서 OrderServiceImpl 구현체를 생성하고 생성자 주입을 통해 FixDiscountPolicy 구현체를 주입하고 있다.
  • 아래 코드는 수정된 OrderServiceImpl 클래스다. DiscountPolicy만 의존관계를 갖고 나머지는 생성자 주입을 통해 구현체를 관리한다.

OrderServiceImpl과 Appconfig 클래스는 각각 역할을 구분하여 책임을 부여하고 있다. 이를 통해 각 클래스가 어떤 역할을 하는지 명확하게 확인할 수 있고 변경사항에도 대비할 수 있다.

 

Config클래스의 추가적인 기능 설명

더보기

Spring에서 Configuration 클래스는 애플리케이션의 구성 정보를 포함하고, 빈(Bean) 설정과 관련된 것들을 정의하는 역할을 합니다. 이 클래스는 Java 기반으로 작성되며, @Configuration 어노테이션을 사용하여 선언됩니다.

주요 목적은 다음과 같습니다:

  1. 빈(Bean) 설정: @Bean 어노테이션을 사용하여 빈 객체를 정의합니다. 빈은 스프링 컨테이너에서 관리되는 객체로, 필요한 곳에서 주입(Dependency Injection) 받을 수 있습니다.
  2. 환경 설정 및 프로퍼티 설정: 데이터베이스 연결 정보, 외부 서비스 URL 등과 같은 환경 설정 정보를 정의할 수 있습니다.
  3. 조건부 빈 설정: 특정 조건에 따라 빈을 설정하거나 제외할 수 있습니다.
  4. 다른 Configuration 클래스와의 조합: 여러 개의 Configuration 클래스를 작성하고, 조합하여 하나의 애플리케이션 컨텍스트를 구성할 수 있습니다.
  5. AOP(Aspect-Oriented Programming) 설정: AspectJ와 같은 AOP를 사용하기 위한 설정을 추가할 수 있습니다.
  6. 컴포넌트 스캔 제어: @ComponentScan 어노테이션을 통해 어느 패키지에서 컴포넌트를 스캔할지 설정할 수 있습니다.
728x90

들어가며


안녕하세요,😎😎😎

3편에 들어서야 드디어 개발과 관련이 있는 정리글을 쓸 수 있게 되었습니다. 필수 아닌 필수(?)라서 기술 블로그를 시작했는데 생각보다 많이 어렵네요,,,

그래도 하루 한 편을 목표로 취업할때까지는 꾸준히 작성해보겠습니다.!

 

회원 도메인 설계


  1. 회원 도메인 요구사항
  2. 회원을 가입하고 조회할 수 있다.
  3. 회원은 일반과 VIP 두 가지 등급이 있다.
  4. 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

클라이언트는 가입과 조회 서비스를 요청할 수 있으며, 멤버 데이터를 저장하고 조회하는 기능을 추상화하여 인터페이스를 통해 정의하고, 이를 구현하는 구체적인 클래스를 통해 실제 기능을 구현하고 사용할 수 있도록 합니다.

회원 클래스 Member Class


가장 기초적인 회원 도메인에 대한 클래스는 멤버 변수/ 생성자/ 게터 및 세터로 구성됩니다.

  1. 멤버 변수는 private로 작성하여 외부로부터 접근을 차단합니다.
  2. 생성자는 멤버 객체를 생성할 때 초기화하는 역할을 합니다.
  3. 게터 및 세터를 통해 외부에서 변수를 설정하고 호출할 수 있습니다.
package hello.core.member;
// Member 변수 생성
public class Member {
    private Long id;
    private String name;
    private Grade grade;
// 생성자
    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }
//get과 set으로 변수 설정 및 호출
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

회원 서비스 인터페이스 Member Service Interface


클라이언트의 가입조회 서비스 요청

서비스 기능은 비교적 간단하다. 가입과 조회를 위해 join 메서드와 findMember 메서드를 이용했다.

그리고 가입을 위해 입력 받은 값이 어딘가 저장되어 있어야 조회도 가능할 것이다.

package hello.core.member;

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

회원 저장소 인터페이스 Member Repository Interface


멤버 데이터를 저장하고 조회하는 기능을 추상화

서비스(Service)와 저장소(Repository)를 분리하여, 각 역할을 명확하게 구분하였고 쉽게 확장이 가능하다.

package hello.core.member;

public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}

구체적인 클래스 생성 MemberServiceImpl


구체적인 클래스를 통해 실제 기능을 구현 하기 위해 회원 저장소를 불변 값으로 놓고 생성자를 통해 의존성을 주입하였다. -> DI원칙에 의해 작성되었으며, 이후 다른 저장소로 교체가 용이합니다.

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    // 생성자를 통해 MemberRepository 주입
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

메모리 저장소 MemoryMemberRepository


데이터베이스가 결정되지 않았으므로, 메모리에 저장하는 방식으로 구현된 것이다.

키-값 쌍을 저장하는 자료구조인 Map을 활용하여 store 변수를 생성, 변수는 HashMap 알고리즘을 통해 초기화한다.

자료구조와 알고리즘에 대해 추가적인 공부가 필요하다..

더보기

HashMap은 해시 함수를 사용하여 빠르게 키-값 쌍을 검색하고 저장하고, TreeMap은 키를 기준으로 정렬하여 저장합니다. LinkedHashMap은 입력된 순서대로 키-값을 유지합니다.

package hello.core.member;

import java.util.HashMap;
import java.util.Map;

public class MemoryMemberRepository implements MemberRepository {


    private static Map<Long, Member> store = new HashMap<>();
    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}

 

728x90

들어가며


안녕하세요,

Spring 프레임워크와 관련해 시리즈로 글을 쓰기로 마음을 먹었는데,, 사실 인프런의 김영한 선생님 커리큘럼을 수강중이라 수강 후기에 가까울 것 같습니다. ㅎㅎ

이전 글에서는 대표적인 용어 개념을 정리한 것 같은데 이번 글에서는  객체 지향 프로그래밍의 개념에 대해 정리를 하려고 합니다. 

 

 

처음 자바 스프링을 공부한다면?Java Spring 프레임워크의 기본

백엔드 개발자를 선택하고 나서 개발자에게 프론트는 js, 백은 Java를 선택해야(?)만 할정도로 한국 사회에서 가장 널리 보급되고 있는 것 같다. 프론트 백엔드 웹 html, css, js (React, Next) Java, Java Spri

wooltech.tistory.com

 

 

객체 지향 = 다형성?


객체 지향 프로그래밍의 정의는 위키백과에 따르면, 아래와 같다.

더보기

객체 지향 프로그래밍(영어: Object-Oriented Programming, OOP)은 컴퓨터 프로그래밍 패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 쉽게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다. 또한 프로그래밍을 더 배우기 쉽게 하고 소프트웨어 개발과 보수를 간편하게 하며, 보다 직관적인 코드 분석을 가능하게 하는 장점이 있다. 그러나 지나친 프로그램의 객체화 경향은 실제 세계의 모습을 그대로 반영하지 못한다는 비판을 받기도 한다.

또한, 자바 언어 설명서에 의하면 객체는 클래스 인스턴스 또는 배열로 정의된다.

An object is a class instance or an array.

 

 

처음 들었을 때는 객체, 클래스, 인스턴스 등 이해되지 않는 말들 투성이였다.

그렇지만 개발자 = 창조자(god)에 비유하면 이해를 도울 수 있으리라 생각한다. 자, 우리는 어플리케이션이라는 세상을 창조하려고 한다. 세상을 구성하고 기능하게 하기 위해서는 피조물(객체)가 필요하다.

 

객체는 추상적, 구체적 정의로 나뉜다. 아래 그림을 예로 들면, K3, 아반떼, 모델3는 모두 자동차면서도 서로 구분이 된다. 여기서 자동차는 추상적 정의다. 우리의 머리 속에만 존재하는 개념.

 

A : 나 '자동차' 살거야.

B : 어떤 자동차?

A : K3!

 

A와 B는 모두 자동차에 대해 이야기를 하고 있지만, 실제로 사용하기 위해서는 '어떤 자동차'라는 구체적인 정의가 필요하다.

다형성은 자동차라는 클래스를 수만가지의 종류를 구현할 수 있음을 의미한다.

프로그래밍 관점에서 클라이언트가 '자동차를 타고 싶어'라고 요청을 보냈다면 K3를 타다가 아반떼를 타고 그리고 모델3로 바꿔 타도 문제가 없다.

 

인터페이스와 상속


그래서 자동차는 단순히 공통분모로 정의되는 개념일까? 

우리가 머리 속에 자동차를 떠올릴 때 생각나는 형태가 있을 것이다. 그 특징들의 차이로 K3, 아반떼, 모델3를 구분할 수 있다. 이 특징들은 매우 중요하고 이를 추상 메서드라고 한다.

추상 메서드는 추상 클래스와 인터페이스를 통해 상속되는 개념인데, 클래스는 하나의 추상 메서드만을 상속하지만, 인터페이스는 여러 개의 추상 메서드를 상속할 수 있다.

 

다만, 추상 메서드는 강제로 하위 클래스에서 구현되기 때문에 안정적인 설계가 중요하다.

글솜씨가 부족해 제대로 전달이 될 지 우려된다...

더보기
  1. 하위 클래스에서의 강제 구현: 추상 메서드는 하위 클래스에서 반드시 구현되어야 하는 메서드입니다. 추상 클래스나 인터페이스에 있는 추상 메서드를 상속받은 클래스는 해당 메서드를 반드시 구현해야 합니다. 이를 통해 특정한 동작이나 기능을 하위 클래스에서 강제할 수 있습니다.
  2. 다형성 구현: 추상 메서드는 다형성(polymorphism)을 구현하는데 사용됩니다. 여러 하위 클래스가 같은 추상 메서드를 각각의 방식으로 구현함으로써, 이들 객체를 동일한 추상 타입으로 다룰 수 있습니다.
  3. 클래스 설계의 기본 틀 제공: 추상 클래스에서는 구현이 필요한 메서드를 제공하면서 동시에 기본적인 구조나 틀을 제공할 수 있습니다. 이를 상속받은 하위 클래스는 이러한 틀을 기반으로 추가적인 동작을 정의할 수 있습니다.

예를 들어, 동물을 나타내는 추상 클래스가 makeSound라는 추상 메서드를 가지고 있을 때, 이를 상속받은 구체적인 동물 클래스에서는 각 동물의 소리를 구현할 수 있습니다. 이로써 동물의 다양성을 표현하고 공통된 특징을 추상 클래스에서 제공할 수 있습니다.

*동물을 키워드로 아래 코드를 보면 이해가 쉬울 것이라 생각한다.

// 동물을 나타내는 인터페이스
interface Animal {
    void makeSound(); // 추상 메서드: 동물의 울음 소리를 나타냄
}

// 각 동물을 구현하는 클래스
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!"); // 강아지는 멍멍 짖음
    }
}

class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹!"); // 고양이는 야옹 소리
    }
}

class Duck implements Animal {
    @Override
    public void makeSound() {
        System.out.println("꽥꽥!"); // 오리는 꽥꽥 소리
    }
}

public class AnimalExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        Animal duck = new Duck();

        dog.makeSound(); // 멍멍!
        cat.makeSound(); // 야옹!
        duck.makeSound(); // 꽥꽥!
    }
}

 

인터페이스의 다중 구현


FlyingFish는 Fly와 Swim 두가지 추상 메서드를 상속 받는다.

// 인터페이스 정의
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// 클래스가 두 인터페이스를 다중으로 구현
class FlyingFish implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("Flying in the air!");
    }

    @Override
    public void swim() {
        System.out.println("Swimming in the water!");
    }
}

public class Main {
    public static void main(String[] args) {
        FlyingFish flyingFish = new FlyingFish();
        flyingFish.fly();  // Flying in the air!
        flyingFish.swim(); // Swimming in the water!
    }
}

 

 

 

 

728x90

백엔드 개발자를 선택하고 나서


개발자에게 프론트는 js, 백은 Java를 선택해야(?)만 할정도로 한국 사회에서 가장 널리 보급되고 있는 것 같다. 

  프론트 백엔드
html, css, js (React, Next) Java, Java Spring, Spring boot // node.js
모바일 Flutter

 

이외에도 많지만 대표적으로 기본이 되는 것들을 추리면 이렇게 되는 것 같다. 위 기술들을 바탕으로 여러 언어와 기술을 습득해가는 방식(?). 모바일은 들어본게 Flutter라...

 

JDK와 IDE 선택

 

JDK는 자바 개발 키트의 약자로 자바 어플리케이션 개발을 위한 기본 도구다.

JDK안에는 JDE(자바 개발 환경), JVM(자바 가상 머신) 등이 포함되어 JAVA를 통해 어플리케이션을 개발할 수 있게 해주는 플랫폼이라고 생각하면 된다.

 

 

JDK를 설치하면 사실상 메모장을 통해서도 자바 어플리케이션을 만들 수 있다. 그러나, 백지 위에 모든 코드를 손수 쓰며 코딩하기에는 생산성이 떨어지므로, IDE라는 통합개발툴을 사용한다.

 

많이 쓰는 Oracle의 open JDK를 설치하였으며, Intellij(인텔리제이)의 Ultimate 버젼을 사용.

WAS(TOMCAT)과 Web서버


 

 

Web서버와 WAS의 차이는 가장 대표적으로는 '어떤 요청을 처리하는가'다.

  • Web서버는 html, css, js등 정적인 요청을 수행하는 역할.
  • WAS는 DB에서 데이터를 가져오거나, API를 호출하는 등 동적인 요청을 해결하는 역할.

위 그림과 같이 일반적으로 클라이언트의 요청을 받아 서버는 연결된 어플리케이션(+데이터베이스)의 동작을 반환한다.

따라서 '어떻게 효율적으로 반환할 수 있지?'라는 생각을 바탕으로 개발을 해나가는 것이 백엔드 개발자의 좋은 자세라고 생각한다.

MVC


좋은 반환(?)의 첫번째는 MVC 패턴이며, Chat-gpt에 따르면, MVC의 정의는 아래와 같다.

동작에 따라 3가지 큰 분류를 통해 로직을 짜는 방법이다. 

Controller가 클라이언트의 요청을 받고 Model에서 데이터를 추출View를 통해 반환한다.

더보기

MVC (Model-View-Controller):

  • 목적: 사용자 인터페이스 및 비즈니스 로직을 구조화하고 분리하기 위한 디자인 패턴입니다.
  • 역할:
    • Model: 데이터와 비즈니스 로직을 담당합니다. 데이터의 상태를 유지하고 업데이트하는 역할을 합니다.
    • View: 사용자에게 보이는 부분을 담당합니다. Model의 데이터를 시각적으로 표현하고 사용자와의 상호작용을 처리합니다.
    • Controller: 사용자의 입력을 받아 Model과 View 사이에서 중개자 역할을 합니다. 입력을 기반으로 Model을 업데이트하고, View를 갱신합니다.
  • 장점: 애플리케이션의 모듈화, 유지보수성, 확장성을 향상시킵니다.

*예시

@Controller
@RequestMapping("/example")
public class ExampleController {

    // "/example/hello"에 대한 GET 요청을 처리하는 메서드
    @GetMapping("/hello")
    public String hello() {
        return "helloPage";
    }

    // "/example/greet"에 대한 POST 요청을 처리하는 메서드
    @PostMapping("/greet")
    public String greet(@RequestParam String name, Model model) {
        model.addAttribute("name", name);
        return "greetPage";
    }
}

 

@Controller라는 어노테이션을 통해 위의 클래스가 컨트롤러임을 지정한다.

@___Mapping은 요청에 따라 메서드를 정하고 return 값을 통해 반환될 View를 지정한다.

 

자세한 내용은 HTTP 정리 글에서 다룰 예정이므로 간략하게 Mapping의 종류만 정리한다.

@GetMapping("/example")
public String example() {
    // ...
}

@PostMapping("/create")
public String create(@RequestBody DataObject data) {
    // ...
}

@PutMapping("/update/{id}")
public String update(@PathVariable Long id, @RequestBody UpdatedDataObject updatedData) {
    // ...
}

@DeleteMapping("/delete/{id}")
public String delete(@PathVariable Long id) {
    // ...
}

API


백엔드 개발자의 주업무라고 해야하나.. 회원, 상품, 주문, 정산 등 API개발을 한다.

하나의 소프트웨어는 컨포넌트의 집합이라고 할 수 있다. 그리고 컨포넌트 간의 통신을 돕는 것이 API의 역할이다.

더보기

API (Application Programming Interface):

  • 목적: 다른 소프트웨어 컴포넌트들이 상호작용하기 위한 인터페이스를 제공하는 도구 또는 규칙의 모음입니다.
  • 역할: 서로 다른 소프트웨어 시스템이나 서비스 간에 통신하고 데이터를 교환할 수 있게 해줍니다. 주로 함수, 메서드, 클래스 등의 형태로 제공됩니다.
  • 종류:
    • Web API: HTTP/HTTPS를 통해 웹에서 서비스되는 API. REST 또는 SOAP을 사용할 수 있습니다.
    • Library API: 라이브러리에서 제공되는 함수 호출 형태의 API.
    • Operating System API: 운영체제에서 제공되는 기능에 접근하기 위한 API.
  • 장점: 다양한 플랫폼 및 언어 간의 통합을 용이하게 하며, 모듈 간에 표준화된 방식으로 상호작용할 수 있도록 도와줍니다.

더보기

컴포넌트 역할과 동작:

  1. 서비스 (Service):
    • 역할: 비즈니스 로직을 수행하는 부분으로, 특정 기능 또는 업무를 담당합니다.
    • 동작: 주로 비즈니스 로직을 구현하고, 해당 기능에 필요한 데이터를 처리하기 위해 리포지토리를 호출합니다.
  2. 리포지토리 (Repository):
    • 역할: 데이터베이스와 관련된 작업을 수행하며, 데이터에 접근하고 변경하는 역할을 합니다.
    • 동작: 데이터의 CRUD(Create, Read, Update, Delete) 작업을 처리하고, 서비스에게 필요한 데이터를 제공합니다.
  3. 컨트롤러 (Controller):
    • 역할: 사용자의 요청을 받고, 해당 요청에 대한 응답을 반환하는 부분입니다. 웹 애플리케이션에서 클라이언트와 상호작용합니다.
    • 동작: HTTP 요청을 받아서 서비스에게 전달하고, 서비스로부터 받은 결과를 적절한 형태로 응답합니다.

*예시

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void registerMember(Member member) {
        // 비즈니스 로직 수행, 예를 들어 중복 회원 검사 등
        if (isDuplicateMember(member)) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
        memberRepository.save(member);
    }

    private boolean isDuplicateMember(Member member) {
        // 중복 여부 검사 로직
    }
}


@Repository
public class MemberRepository {
    private final Map<Long, Member> store = new HashMap<>();
    private long sequence = 0L;

    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}


@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/members/{id}")
    public String getMemberById(@PathVariable Long id, Model model) {
        // 회원 조회 로직
        Member member = memberService.getMemberById(id);
        model.addAttribute("member", member);
        return "memberDetail";
    }

    @PostMapping("/members/new")
    public String registerNewMember(@ModelAttribute MemberForm form) {
        // 회원 가입 로직
        Member member = new Member();
        member.setName(form.getName());
        memberService.registerMember(member);
        return "redirect:/members/" + member.getId();
    }
}

 

 

ORM과 JPA 그리고 RDBMS


ORM -> JPA -> Spring Data JPA

-> 객체와 DB를 Mapping해서 SQL문 없이 반복적인 CRUD를 구현.

더보기

Spring Data JPA에서는 JpaRepository 인터페이스를 확장하여 Repository를 생성할 때 이미 기본적인 CRUD 메서드들을 제공받게 됩니다. 개발자는 이러한 메서드를 사용함으로써 별도의 쿼리 메서드 작성이나 엔터티 매니저를 직접 다루지 않아도 됩니다. Spring Data JPA는 이러한 구현체를 런타임에 자동으로 생성하여 빈으로 등록합니다.

JPA와 Spring Data JPA 차이


JPA 예시:

//엔터티 클래스 (Entity Class):
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // Getters, setters, 기타 메서드
}

//JPA를 사용한 Repository:
import javax.persistence.EntityManager;
import java.util.List;

public class MemberRepository {
    private final EntityManager em;

    public MemberRepository(EntityManager em) {
        this.em = em;
    }

    public void save(Member member) {
        em.persist(member);
    }

    public Member findById(Long id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("SELECT m FROM Member m", Member.class)
                .getResultList();
    }

    // 기타 필요한 메서드들
}

//엔터티 매니저 활용:
EntityManager em = // 엔터티 매니저 생성 (예: EntityManagerFactory로부터)
MemberRepository repository = new MemberRepository(em);

Member member = new Member();
member.setName("John Doe");

repository.save(member);

Member foundMember = repository.findById(member.getId());
List<Member> allMembers = repository.findAll();

Spring Data JPA 예시:

//엔터티는 동일
//Spring Data JPA Repository:
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    // JpaRepository에서 기본적인 CRUD 메서드들을 이미 제공받음
}

//Spring Data JPA 활용:
MemberRepository repository = // Spring이 자동으로 구현한 Repository 빈을 주입받음

Member member = new Member();
member.setName("John Doe");

repository.save(member);

Member foundMember = repository.findById(member.getId()).orElse(null);
List<Member> allMembers = repository.findAll();

@Bean 등록 - Config


@Configuration을 가진 클래스는 주로 @Bean을 등록해 DI(의존성 주입)을 한다.

 

더보기

 @Bean을 활용하는 경우

  1. 의존성 주입 필요: 다른 빈이나 구성 요소에서 해당 서비스를 사용해야 할 때, 빈으로 등록하여 의존성 주입을 받을 수 있습니다.
  2. AOP(Aspect-Oriented Programming)를 활용: 스프링의 AOP를 사용하려면 빈으로 등록된 객체에 대해서만 AOP를 적용할 수 있습니다.
  3. 트랜잭션 관리: 서비스 메서드에 트랜잭션을 적용하려면 스프링의 트랜잭션 관리를 위해 빈으로 등록하는 것이 효과적입니다.
  4. 테스트 용이성: 빈으로 등록된 서비스는 테스트에서 쉽게 목 객체(Mock Object)로 대체하거나 스프링 테스트 컨텍스트를 활용하여 테스트할 수 있습니다.
  5. 컨트롤러에서 사용: 스프링 MVC에서 컨트롤러에서 서비스를 주입받아 사용할 때는 빈으로 등록하는 것이 일반적입니다.

Junit5


JUnit은 테스트 주도 개발(TDD) 및 소프트웨어 개발의 품질 향상을 위해 많이 사용되는 도구 중 하나로 단위 테스트를 통해 개발 코드를 리뷰한다.

 

*예시

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyTest {
    @Test
    public void testAddition() {
        assertEquals(4, 2 + 2);
    }
}

 

 

 

 

+ Recent posts