no image
[Spring 입문] 스프링 빈과 의존관계
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 아래의 글에 연관이 있는 포스팅입니다. 2024.01.21 - [Java Category/Spring] - [Spring 입문] 회원 관리 예제 - 백엔드 개발 회원 가입하고 그 결과를 html을 통해 화면에 띄우려고 하는데, 그러러면 Controller와 View template가 필요하다. MemberController를 만들어서 사용해야 하는데, MemberController가 MemberService를 통해서 회원 가입을 하고, 데이터 조회를 해야 한다. 이런 것을 "MemberController가 MemberService를 의존한다" 라고 표현한다. 따라서 회원 컨트롤러가 회원서비스와 회원 리포지토리를..
2024.01.22
no image
[Java] 리스트 구현(SLL, DLL)
단일 연결 리스트(Singly Linked List) 직접 구현 노드 class Node { E data; Node next; Node(E data) { this.data = data; this.next = null; } } 노드 추가 //리스트의 가장 뒷쪽에 데이터 추가 public void add(E data) { Node newNode = new Node(data); if (head == null) head = newNode; else { Node currentHead = head; while (currentHead.next != null) currentHead = currentHead.next; currentHead.next = newNode; } } 노드 삽입 //리스트의 원하는 인덱스에 데이터..
2024.01.21
no image
[Spring 입문] 회원 관리 예제 - 백엔드 개발
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 비즈니스 요구사항 정리 데이터 : 회원 ID, 이름 기능 : 회원 등록(단, 중복 이름 허용X), 회원 조회 아직 DB가 선정되지 않은 상황 (가상의 시나리오) 일반적인 웹 애플리케이션 계층 구조 컨트롤러: 웹 MVC의 컨트롤러 역할 서비스: 핵심 비즈니스 로직 구현 (예를 들어 회원 중복 가입 허용 안됨과 같은 로직들 또한 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 객체) 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리 (아직 DB가 선정되지 않은 상태로 가정) 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨 클..
2024.01.21
no image
[Spring 입문] 스프링 웹 개발 기초
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 웹을 개발하는 방법에는 크게 세 가지가 있다. 1. 정적 컨텐츠 (static contents) 서버에서 따로 가공을 거치지 않고 파일을 웹 브라우저에 그대로 보내는 방식이다. 2. MVC와 템플릿 엔진(template engine) 과거의 JSP, PHP 같은 것들이 template engine이다. 정적 컨텐츠처럼 html을 웹 브라우저에 그대로 보내는 것이 아니라, 데이터를 가공 해서 html을 동적으로 바꿔서 웹 브라우저에 보내는 일을 한다. 이런 방법을 사용하기 위해서는 Model, View, Controller가 필요한데, 이를 합쳐서 MVC라고 부른다. 정적 컨텐츠와의 차이점 html을 웹 페이지에..
2024.01.20
no image
[Spring 입문] 프로젝트 환경 설정
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 라이브러리 살펴보기 External Libraries Project 창에서 "External Libraries"를 찾을 수 있는데, 이 External Libraries를 보면 가져온 라이브러리들이 무엇인지 볼 수 있다. 해당 폴더를 펼치면 어떤 라이브러리들이 포함되어 있는지 알 수 있다. 하지만 항목들이 너무 많기 때문에 뭐가 뭔지 알기가 힘들다. 이럴 때에는 Gradle 탭을 활용하면 좋다. Gradle 탭 Gradle 탭의 Dependencies를 보면 선택한 라이브러리를 볼 수 있다. Denpendencies는 의존성이라는 뜻인데, gradle(maven)의 의존 관계를 관리해준다. Dependencie..
2024.01.19
no image
[JavaScript] 클래스
이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다. 클래스 기본 형태 다른 프로그래밍 언어와 비슷한 형태이다. private 클래스 내부에서 사용되는 # 기호로 시작하는 이름으로 선언된 멤버가 private 멤버로 취급된다. 이러한 private 멤버는 클래스 외부에서 직접 접근할 수 없다. static JavaScript에서 static 키워드는 클래스의 정적(static) 멤버를 정의할 때 사용된다. 정적 멤버는 클래스 자체에 속하며 인스턴스를 생성하지 않고도 클래스에서 직접 접근할 수 있는 멤버이다. 상속 JavaScript에서 클래스를 정의할 때, extends 키워드를 사용하여 다른 클래스를 상속할 수 있다. 만약 부모 ..
2024.01.18
no image
[JavaScript] 예외처리
이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다. try - catch - finally 문 try{ //예외 발생 가능성이 있는 코드 } catch(exception){ //예외처리를 할 코드 } finally{ //무조건적으로 실행될 코드 } finally 구문은 catch 구문에서 return, break, continue 등을 만나더라도 실행된다. 또한 파일 입출력등에서 파일을 사용 도중에 오류를 만나면 리소스를 자동으로 닫는 상황에서도 사용된다. 예외 객체 예외 객체 속성 속성이름 설명 name 예외 이름 message 예외 메시지 예외 강제 발생 상황에 따라서 예외를 강제로 발생시켜야 하는 경우도 있다. 자바스크립트는..
2024.01.18
no image
[JavaScript] 문서 객체 모델(DOM:Document Object Model)
이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다. 문서 객체 조작 DOMContentLoaded DOMContentLoaded는 웹 페이지의 모든 HTML 요소가 브라우저에 의해 로드되고 파싱되었을 때 발생하는 이벤트이다. 이는 이미지, 스타일시트 등의 외부 자원을 기다리지 않고, HTML 요소들 자체가 다 로드되었을 때 발생한다. 이벤트가 발생하면 JavaScript에서 이를 감지하여 특정 기능을 실행하거나 초기화 작업을 할 수 있다. 페이지의 모든 구성 요소가 준비된 상태에서 JavaScript 코드를 실행할 수 있는 타이밍을 제공하여, 더 나은 사용자 경험을 만들거나 웹 애플리케이션을 초기화하는 데 활용할 수 있다. 문서 ..
2024.01.17

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


아래의 글에 연관이 있는 포스팅입니다.

2024.01.21 - [Java Category/Spring] - [Spring 입문] 회원 관리 예제 - 백엔드 개발

회원 가입하고 그 결과를 html을 통해 화면에 띄우려고 하는데, 그러러면 Controller와 View template가 필요하다.

MemberController를 만들어서 사용해야 하는데, MemberController가 MemberService를 통해서 회원 가입을 하고, 데이터 조회를 해야 한다.

이런 것을 "MemberController가 MemberService를 의존한다" 라고 표현한다.

따라서 회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있도록 의존관계를 설정하는 것을 의존성 주입(Dependency Injection)이라고 한다.

스프링 빈과 의존성 주입과의 관계
의존성 주입(Dependency Injection, DI)과 스프링 빈과의 관계는 밀접하게 연관되어 있습다.
스프링은 IoC(Inversion of Control) 컨테이너를 제공하여 객체의 생명주기와 의존성을 관리한다.
그 중에서도 DI는 스프링에서 객체 간의 의존성을 해결하고 관리하기 위한 주요 기능 중 하나이다.

스프링 빈(Bean)
스프링 컨테이너가 관리하는 자바 객체를 뜻하며, 하나 이상의 빈(Bean)을 관리한다.
빈은 인스턴스화된 객체를 의미하며, 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
스프링 빈은 IoC 컨테이너에 의해 생성, 관리되며, 스프링 빈을 만들려면 해당 클래스에 @Component, @Service, @Repository, @Controller 등과 같은 어노테이션을 부여하거나 XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않는다.

의존성 주입(Dependency Injection)
의존성 주입은 한 객체가 다른 객체에 의존할 때, 그 의존성을 외부에서 주입하는 디자인 패턴이다.
스프링에서는 주로 생성자 주입, 필드 주입, 메서드 주입 등의 방식으로 의존성을 주입한다.
주입 받을 객체는 스프링에서 관리되는 빈이어야 한다.

@Service // 스프링 빈으로 등록
public class MyService {

    private final MyRepository myRepository;

    // 생성자 주입
    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    // ...
}​

위의 코드에서 MyService는 @Service 어노테이션으로 스프링 빈으로 등록되었고, MyRepository 타입의 의존성이 생성자 주입을 통해 주입되었다.
스프링 IoC 컨테이너는 MyService를 생성할 때 필요한 MyRepository 빈을 찾아서 주입한다.

이렇게 함으로써 스프링은 개발자가 객체 간의 의존성을 관리하고, 런타임에 의존성을 주입함으로써 코드의 유연성과 테스트 용이성을 높일 수 있도록 도와준다.

Inversion of Control (IoC)
소프트웨어 디자인 패턴 중 하나로, 제어의 역전이라고도 불린다.
이 패턴은 프로그램의 흐름이 개발자가 작성한 코드가 아니라 외부 컨테이너에 의해 결정된다는 개념을 기반으로 한다.
일반적으로 프로그램의 제어 흐름은 개발자가 코드를 작성하여 정의한다.
그러나 IoC에서는 제어 흐름의 일부 또는 전부가 외부의 프레임워크나 컨테이너에 의해 관리되고 결정된다.
이는 주로 의존성 주입(Dependency Injection)이라는 형태로 나타난다.
IoC의 핵심 아이디어는 개발자가 코드를 작성할 때가 아니라, 프레임워크 또는 컨테이너가 애플리케이션의 객체들을 생성하고 관리할 때이다. 이렇게 하면 객체 간의 결합도가 낮아져 유연성과 확장성이 증가하며, 테스트하기 쉬운 코드를 작성할 수 있게 된다.
Spring 프레임워크는 IoC의 원칙을 따르는 대표적인 프레임워크 중 하나이다.
Spring IoC 컨테이너는 객체의 생성, 초기화, 소멸, 의존성 주입 등을 관리하고, 애플리케이션의 제어 흐름을 개발자에게서 컨테이너로 역전시킨다.
이는 코드의 모듈화와 재사용성을 높이며, 유지보수가 용이한 시스템을 구축하는데 도움이 된다.

 

일단은 컨트롤러를 추가하자

package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {

}
@Controller
spring이 실행될 때, spring container가 생긴다.
그 때, @Controller이 적혀있다면 spring container에 해당 객체를 넣어두고, spring이 관리를 한다.
이후 Controller가 필요할 때 container에서 지정된 Controller를 사용한다.
이를 "spring container에서 spring bean이 관리된다" 라고 표현한다.

 

스프링 빈을 등록하는 방법은 아래와 같이 2가지 방법이 있다.

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록

 

컴포넌트 스캔과 자동 의존관계 설정

Autowired을 이용하여 MemberController와 MemberService 연결

MemberController가 MemberService를 사용해야 하는데, new 키워드를 이용하여 MemberService 객체를 생성하여 사용할 수도 있다.
하지만 MemberService를 spring container에 등록을 해서 spring이 관리를 하도록 해야한다. spring container에 등록하면 하나의 객체만 등록이 된다. (singleton 패턴)
이렇게 해야 다른 Controller에서도 MemberService를 가져와 사용할 수 있다.

스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록 해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다.
설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

MemberService를 spring container에 연결하는 방법은 아래와 같다.

  1. MemberController 클래스에서 memberService 클릭 - Alt+Enter - Add Constructor Parameter를 선택한다.
  2. 생성자에 @Autowired 어노테이션을 붙여준다.
package hello.hellospring.controller;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class MemberController {
    private final MemberService memberService;

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

 

여기까지 했다면 MemberService를 spring container에 연결은 했지만, memberService가 아직 스프링 빈으로 등록되어있지 않다.

@Autowired 어노테이션이 있다면 spring container에서 해당 객체를 찾은 후 연결시켜준다.

그러려면 MemberService가 spring container에 스프링 빈으로 등록이 되어 있어야 한다.

하지만 MemberService는 순수한 java class이기 때문에 spring bean으로 등록되어 있지 않다.

 

어노테이션을 통해 Spring Container에 등록하기

@Service와 @Repository 어노테이션을 통해서  spring container에 등록해야 한다.
먼저 MemberService 클래스에 가서 @Service 어노테이션을 붙여준다.
그리고 MemoryMemberRepository에는 @Repository 어노테이션을 붙여준다.
여기까지 하면 MemberController, MemberService, MemoryMemberRepository가 spring container에 등록된다.

추가적으로 MemberService의 생성자를 살펴보면, MemberRepository를 할당받는데 @Autowired를 통해 spring container에 등록된 MemoryMemberRepository 객체를 연결시켜주어야 한다.

생성자에 @Autowired 를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입 한다.
생성자가 1개만 있으면 @Autowired 는 생략할 수 있다.

Controller, Service, Repository는 정형화 되어 있는 패턴이다.

Controller를 통해서 외부 요청을 받고, Service에서 비지니스 로직을 만들고, Repository에서 데이터를 저장하는 패턴이다.

 

MemberController

@Controller
public class MemberController {
    private final MemberService memberService;

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

 

MemberService

@Service
public class MemberService {

    private final MemberRepository memberRepository;

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

 

MemoryMemberRepository

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

 

컴포넌트 스캔

@Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다.

@Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.

@Component 를 포함하는 다음 어노테이션도 스프링 빈으로 자동 등록된다

  • @Controller
  • @Service
  • @Repository
컴포넌트 스캔 시, 객체는 기본적으로 싱글톤(singleton)으로 등록하고, 하나의 객체를 공유한다.
컴포넌트 스캔은 기본적으로 main 메서드가 있는 package의 하위 package들만을 스캔하고 등록한다.

즉, Component와 관련된 어노테이션이 있으면, 해당 객체를 모두 하나씩 생성해서 spring container에 등록을 하고, @Autowired를 통해 객체들을 연결해준다.

 

자바 코드로 직접 스프링 빈 등록

컨트롤러는 아래와 같이 되어있어야 한다.

@Controller
public class MemberController {
    private final MemberService memberService;
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

service 패키지에 SpringConfig 클래스를 생성하고, @Configuration 어노테이션을 추가한다.



여기서 @Configuration은 설정 파일을 만들거나 Bean을 등록하기 위한 어노테이션이다.

@Bean을 사용하는 클래스의 경우 반드시 @Configuration을 같이 사용해야 한다.

@Configuration
public class SpringConfig {

}

 

SpringConfig 클래스에 다음과 같은 코드를 작성한다.

@Bean
public MemberService memberService() {
    return new MemberService();
}

@Bean은 spring bean으로 등록한다는 뜻이다.
이렇게 하면 @Configuration에서 @Bean을 읽고, spring bean에 등록하라는 뜻이라고 인식한다.
그리고 return 값을 통해 받은 MemberService를 spring bean에 등록을 해준다.

MemberService의 생성자는 MemberRepository 인터페이스 변수를 매개변수로 받아야 한다.
따라서 MemberRepository인터페이스 타입의 구현 객체를 반환하는 함수도 spring bean에 등록해야 한다.

@Bean
public MemberRepository memberRepository(){
    return new MemoryMemberRepository();
}

이제 MemoryMemberRepository(인터페이스 구현 객체)를 반환하는 함수 memberService를 생성했다.

다음으로 MemberService의 생성자를 수정해야 한다.

@Bean
public MemberService memberService() {
    return new MemberService(memberRepository());
}

이렇게 하면MemberService의 생성자로 전달한다.
최종적으로 spring bean에 정상적으로 MemberService 가 등록된다.

 

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}
정리
1. @Configuration 어노테이션을 통해서 @Bean 어노테이션이 있는 것(memberService(), memberRepository())들을  spring bean에 등록하게 된다.
2. 컨트롤러의 생성자가 MemberService 객체를 요구한다.
3. memberRepository() 메소드에 의해서 구현 객체가 리턴되고, 리턴된 객체를 memberService()의 리턴값으로 전달하므로써 생성자에 @Autowired 어노테이션으로 연결한 것과 동일한 효과가 난다.
4. memberService()가 MemberService 객체를 컨트롤러의 생성자에 전달하므로써 최종적으로 스프링 컨테이너에 아래와 같은 스프링 빈들이 등록된다.

 

의존성 주입(DI)의 세 가지 방법

Field 주입

멤버 변수의 앞에 @Autowired 어노테이션을 붙인 모양이다. 
하지만 이 방법을 사용하면 IntelliJ에서도 다른 방법을 추천하며, 별로 추천하지 않는 방법이다.
필드 주입은 spring이 처음 실행할 때에만 spring container에서 받아와서 넣어주고, 중간에 값을 바꿀 수 있는 방법이 없다.

@Autowired private MemberService memberService;

 

Setter 주입

이 방법의 단점은 setter가 public으로 설정되어 있기 때문에, 누군가 MemberController를 호출 했을 때 의도치 않게 MemberService를 수정할 수 있다는 점이다.
spring bean은 대부분 한 번 설정되고 나면, 이후에 수정할 일이 거의 없는데 public으로 노출되어 있어 문제가 생길 가능성이 있다.

@Controller
public class MemberController {
    private MemberService memberService;

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

 

Constructor 주입

최근에는 생성자(Constructor)를 이용한 주입을 권장한다.
생성자를 이용한 주입을 하게 되면, 처음 어플리케이션이 조립된다고 표현하는 기점(spring bean이 세팅되는 시점)에 Autowired를 통해서 한 번 할당되고 끝난다.
조립 시점에 생성자를 통해 한 번만 할당을하고 마무리가 되기 때문에 public으로 설정되어 있는 setter보다 안전하다.

@Controller
public class MemberController {
    private final MemberService memberService;

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

 

의존 관계가 Runtime 중에 동적으로 변하는 경우는 거의 없으므로(사실 아예 없다) 생성자 주입을 권장한다.
만일 정말 의존 관계를 바꿔야 하는 상황이라면, SpringConfig 파일을 수정하고 서버를 다시 올리는 것이 바람직하다.
실무에서는 주로 정형화된 Controller, Service, Repository 같은 코드는 컴포넌트 스캔을 사용한다.
정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정(SpringConfig)를 통해 spring bean으로 등록한다.

단일 연결 리스트(Singly Linked List)

 

직접 구현

노드

class Node<E>
{
    E data;
    Node<E> next;

    Node(E data)
    {
        this.data = data;
        this.next = null;
    }
}

 

노드 추가

//리스트의 가장 뒷쪽에 데이터 추가
public void add(E data)
{
    Node<E> newNode = new Node<>(data);
    if (head == null) head = newNode;
    else
    {
        Node<E> currentHead = head;
        while (currentHead.next != null) currentHead = currentHead.next;
        currentHead.next = newNode;
    }
}

 

노드 삽입

//리스트의 원하는 인덱스에 데이터 삽입
public void insert(E data, int idx)
{
    if (head == null)
    {
        add(data);
        return;
    }
    Node<E> newNode = new Node<>(data);
    Node<E> currentNode = head;
    if (currentNode != null && idx == 0)
    {
        newNode.next = head;
        head = newNode;
        return;
    }
    for (int i = 0; i < idx - 1; ++i)
    {
        if (currentNode == null)
        {
            System.out.println("Error");
            return;
        }
        currentNode = currentNode.next;
    }
    if (currentNode == null)
    {
        System.out.println("Invalid index");
        return;
    }
    newNode.next = currentNode.next;
    currentNode.next = newNode;
}

 

노드 삭제

//앞쪽의 데이터 삭제
public void delete()
{
    if (head == null) System.out.println("Empty");
    else head = head.next;
}

//특정 데이터의 내용을 포함하는 노드 삭제
public void delete(E data)
{
    if (head == null) System.out.println("Empty");
    else if (head.data.equals(data)) head = head.next;
    else
    {
        Node<E> currentHead = head;
        while (currentHead.next != null && !currentHead.next.data.equals(data))
        {
            currentHead = currentHead.next;
        }
        if (currentHead.next != null) currentHead.next = currentHead.next.next;
        else currentHead.next = null;
    }
}

 

노드 검색

//해당 데이터가 존재하는지 아닌지 확인
public void search(E data)
{
    if (head == null)
    {
        System.out.println("Invalid");
        return;
    }
    else
    {
        Node<E> currentHead = head;
        while (currentHead != null && !currentHead.data.equals(data))
        {
            currentHead = currentHead.next;
        }
        if (currentHead == null)  System.out.println("Invalid");
        else System.out.println("Valid");
    }
}

 

노드 출력

public void printList()
{
    Node<E> currentNode = head;
    if (currentNode == null)
    {
        System.out.println("Empty");
        return;
    }
    while (currentNode != null)
    {
        System.out.print(currentNode.data + " ");
        currentNode = currentNode.next;
    }
    System.out.println();
}

 

전체 소스코드

public class SinglyLinkedList<E>
{
    class Node<E>
    {
        E data;
        Node<E> next;

        Node(E data)
        {
            this.data = data;
            this.next = null;
        }
    }

    private Node<E> head;

    SinglyLinkedList() { this.head = null; }

    public void printList()
    {
        Node<E> currentNode = head;
        if (currentNode == null)
        {
            System.out.println("Empty");
            return;
        }
        while (currentNode != null)
        {
            System.out.print(currentNode.data + " ");
            currentNode = currentNode.next;
        }
        System.out.println();
    }

    //리스트의 가장 뒷쪽에 데이터 추가
    public void add(E data)
    {
        Node<E> newNode = new Node<>(data);
        if (head == null) head = newNode;
        else
        {
            Node<E> currentHead = head;
            while (currentHead.next != null) currentHead = currentHead.next;
            currentHead.next = newNode;
        }
    }

    //리스트의 원하는 인덱스에 데이터 삽입
    public void insert(E data, int idx)
    {
        if (head == null)
        {
            add(data);
            return;
        }
        Node<E> newNode = new Node<>(data);
        Node<E> currentNode = head;
        if (currentNode != null && idx == 0)
        {
            newNode.next = head;
            head = newNode;
            return;
        }
        for (int i = 0; i < idx - 1; ++i)
        {
            if (currentNode == null)
            {
                System.out.println("Error");
                return;
            }
            currentNode = currentNode.next;
        }
        if (currentNode == null)
        {
            System.out.println("Invalid index");
            return;
        }
        newNode.next = currentNode.next;
        currentNode.next = newNode;
    }

    //앞쪽의 데이터 삭제
    public void delete()
    {
        if (head == null) System.out.println("Empty");
        else head = head.next;
    }

    //특정 데이터의 내용을 포함하는 노드 삭제
    public void delete(E data)
    {
        if (head == null) System.out.println("Empty");
        else if (head.data.equals(data)) head = head.next;
        else
        {
            Node<E> currentHead = head;
            while (currentHead.next != null && !currentHead.next.data.equals(data))
            {
                currentHead = currentHead.next;
            }
            if (currentHead.next != null) currentHead.next = currentHead.next.next;
            else currentHead.next = null;
        }
    }

    //해당 데이터가 존재하는지 아닌지 확인
    public void search(E data)
    {
        if (head == null)
        {
            System.out.println("Invalid");
            return;
        }
        else
        {
            Node<E> currentHead = head;
            while (currentHead != null && !currentHead.data.equals(data))
            {
                currentHead = currentHead.next;
            }
            if (currentHead == null)  System.out.println("Invalid");
            else System.out.println("Valid");
        }
    }

    public static void main(String args[])
    {
        SinglyLinkedList sll = new SinglyLinkedList();
        sll.add(1);
        sll.add(2);
        sll.add(3);
        sll.add(4);
        sll.insert(0, 0);
        sll.insert(0, 2);
        sll.insert(0, 4);
        sll.delete();
        sll.delete(1);
        sll.delete(0);
        sll.printList();
    }
}

 

책 코드

책 : Do it! 자료구조와 알고리즘 입문 자바편

 

ArrayLinkedList.java

// 연결 리스트 클래스(배열 커서 버전)

import java.util.Comparator;

public class ArrayLinkedList<E> {

    //--- 노드 ---//
    class Node<E> {
        private E data;          // 데이터
        private int next;        // 리스트의 뒤쪽포인터
        private int dnext;       // 프리 리스트의 뒤쪽포인터

        //--- data와 next를 설정 ---//
        void set(E data, int next) {
            this.data = data;
            this.next = next;
        }
    }

    private Node<E>[] n;        // 리스트 본체
    private int size;           // 리스트 크기(최대 데이터 개수)
    private int max;            // 사용 중인 꼬리 record
    private int head;           // 머리노드
    private int crnt;           // 선택 노드
    private int deleted;        // 프리 리스트의 머리노드
    private static final int NULL = -1;    // 뒤쪽노드가 없음 / 리스트가 가득 참

    //--- 생성자(constructor) ---//
    public ArrayLinkedList(int capacity) {
        head = crnt = max = deleted = NULL;
        try {
            n = new Node[capacity];
            for (int i = 0; i < capacity; i++)
                n[i] = new Node<E>();
            size = capacity;
        }
        catch (OutOfMemoryError e) {        // 배열 생성에 실패
            size = 0;
        }
    }

    //--- 다음에 삽입하는 record의 인덱스를 구함 ---//
    private int getInsertIndex() {
        if (deleted == NULL) {                    // 삭제 record가 존재하지 않음
            if (max < size)
                return ++max;                     // 새 record를 사용
            else
                return NULL;                      // 크기 넘침(over)
        } else {
            int rec = deleted;                    // 프리 리스트에서
            deleted = n[rec].dnext;               // 머리 rec을 꺼냄
            return rec;
        }
    }

    //--- record idx를 프리 리스트에 등록 ---//
    private void deleteIndex(int idx) {
        if (deleted == NULL) {                    // 삭제 record가 존재하지 않음
            deleted = idx;                        // idx를 프리 리스트의
            n[idx].dnext = NULL;                  // 머리에 등록
        } else {
            int rec = deleted;                    // idx를 프리 리스트의
            deleted = idx;                        // 머리에 삽입
            n[rec].dnext = rec;
        }
    }

    //--- 노드를 검색 ---//
    public E search(E obj, Comparator<? super E> c) {
        int ptr = head;                                    // 현재 스캔 중인 노드

        while (ptr != NULL) {
            if (c.compare(obj, n[ptr].data) == 0) {
                crnt = ptr;
                return n[ptr].data;                   // 검색 성공
            }
            ptr = n[ptr].next;                        // 뒤쪽 노드 선택
        }
        return null;                                  // 검색 실패
    }

    //--- 머리노드 삽입 ---//
    public void addFirst(E obj) {
        int ptr = head;                                // 삽입 전의 머리노드
        int rec = getInsertIndex();
        if (rec != NULL) {
            head = crnt = rec;                         // 제 rec 번째 레코드에 삽입
            n[head].set(obj, ptr);
        }
    }

    //--- 꼬리노드 삽입 ---//
    public void addLast(E obj) {
        if (head == NULL)                                // 리스트가 비어있으면
            addFirst(obj);                               // 머리에 삽입
        else {
            int ptr = head;
            while (n[ptr].next != NULL)
                ptr = n[ptr].next;
            int rec = getInsertIndex();
            if (rec != NULL) {                        // 제 rec 번째 레코드에 삽입
                n[ptr].next = crnt = rec;
                n[rec].set(obj, NULL);
            }
        }
    }

    //--- 머리노드 삭제 ---//
    public void removeFirst() {
        if (head != NULL) {                            // 리스트가 비어있지 않으면
            int ptr = n[head].next;
            deleteIndex(head);
            head = crnt = ptr;
        }
    }

    //--- 꼬리노드 삭제 ---//
    public void removeLast() {
        if (head != NULL) {
            if (n[head].next == NULL)            // 노드가 하나만 있으면
                removeFirst();                   // 머리노드 삭제
            else {
                int ptr = head;                  // 스캔 중인 노드
                int pre = head;                  // 스캔 중인 노드의 앞쪽노드

                while (n[ptr].next != NULL) {
                    pre = ptr;
                    ptr = n[ptr].next;
                }
                n[pre].next = NULL;                    // pre는 삭제 뒤의 꼬리 노드
                deleteIndex(pre);
                crnt = pre;
            }
        }
    }

    //--- 레코드 p를 삭제 ---//
    public void remove(int p) {
        if (head != NULL) {
            if (p == head)                                // p가 머리 노드이면
                removeFirst();                            // 머리노드 삭제
            else {
                int ptr = head;

                while (n[ptr].next != p) {
                    ptr = n[ptr].next;
                    if (ptr == NULL) return;    // p가 리스트에 없음
                }
                n[ptr].next = NULL;
                deleteIndex(ptr);
                n[ptr].next = n[p].next;
                crnt = ptr;
            }
        }
    }

    //--- 선택 노드 삭제 ---//
    public void removeCurrentNode() {
        remove(crnt);
    }

    //--- 전체 노드 삭제 ---//
    public void clear() {
        while (head != NULL)                        // 비게 될 때까지
            removeFirst();                          // 머리 노드 삭제
        crnt = NULL;
    }

    //--- 선택 노드를 하나 뒤쪽으로 진행 ---//
    public boolean next() {
        if (crnt == NULL || n[crnt].next == NULL)
            return false;                                    // 나아갈 수 없음
        crnt = n[crnt].next;
        return true;
    }

    //--- 선택 노드 표시 ---//
    public void printCurrentNode() {
        if (crnt == NULL)
            System.out.println("선택 노드가 없습니다.");
        else
            System.out.println(n[crnt].data);
    }

    //--- 전체 노드 표시 ---//
    public void dump() {
        int ptr = head;

        while (ptr != NULL) {
            System.out.println(n[ptr].data);
            ptr = n[ptr].next;
        }
    }
}

 

ArrayLinkedListTester.java

// 선형리스트 클래스 ArrayLinkedList<E>의 사용 예

import java.util.Scanner;
import java.util.Comparator;

class ArrayLinkedListTester {
    static Scanner stdIn = new Scanner(System.in);

    //--- 데이터(회원번호+이름) ---//
    static class Data {
        static final int NO   = 1;        // 번호를 읽어 들일까요?
        static final int NAME = 2;        // 이름을 읽어 들일까요?

        private Integer no;                // 회원번호
        private String  name;              // 이름

        //--- 문자열 표현을 반환 ---//
        public String toString() {
            return "(" + no + ") " + name;
        }

        //--- 데이터를 읽어 들임 ---//
        void scanData(String guide, int sw) {
            System.out.println(guide + "할 데이터를 입력하세요.");

            if ((sw & NO) == NO) {
                System.out.print("번호: ");
                no = stdIn.nextInt();
            }
            if ((sw & NAME) == NAME) {
                System.out.print("이름: ");
                name = stdIn.next();
            }
        }

        //--- 회원번호로 순서를 매기는 comparator  ---//
        public static final Comparator<Data> NO_ORDER =
                                                                                    new NoOrderComparator();

        private static class NoOrderComparator implements Comparator<Data> {
            public int compare(Data d1, Data d2) {
                return (d1.no > d2.no) ? 1 : (d1.no < d2.no) ? -1 : 0;
            }
        }

        //--- 이름으로 순서를 매기는 comparator  ---//
        public static final Comparator<Data> NAME_ORDER =
                                                                                    new NameOrderComparator();

        private static class NameOrderComparator implements Comparator<Data> {
            public int compare(Data d1, Data d2) {
                return d1.name.compareTo(d2.name);
            }
        }
    }

    //--- 메뉴 열거형 ---//
    enum Menu {
        ADD_FIRST(  "머리 노드 삽입"),
        ADD_LAST(   "꼬리 노드 삽입"),
        RMV_FIRST(  "머리 노드 삭제"),
        RMV_LAST(   "꼬리 노드 삭제"),
        RMV_CRNT(   "선택 노드 삭제"),
        CLEAR(      "전체 노드 삭제"),
        SEARCH_NO(  "번호 검색"),
        SEARCH_NAME("이름 검색"),
        NEXT(       "선택 노드 진행"),
        PRINT_CRNT( "선택 노드 표시"),
        DUMP(       "전체 노드 표시"),
        TERMINATE(  "종료");

        private final String message;                // 표시할 문자열

        static Menu MenuAt(int idx) {                // 순서가 idx번째인 열거를 반환
            for (Menu m : Menu.values())
                if (m.ordinal() == idx)
                    return m;
            return null;
        }

        Menu(String string) {                        // 생성자(constructor)
            message = string;
        }

        String getMessage() {                        // 표시할 문자열을 반환
            return message;
        }
    }

    //--- 메뉴 선택 ---//
    static Menu SelectMenu() {
        int key;
        do {
            for (Menu m : Menu.values()) {
                System.out.printf("(%d) %s  ", m.ordinal(), m.getMessage());
                if ((m.ordinal() % 3) == 2 &&
                      m.ordinal() != Menu.TERMINATE.ordinal())
                    System.out.println();
            }
            System.out.print(" : ");
            key = stdIn.nextInt();
        } while (key < Menu.ADD_FIRST.ordinal() || 
                                            key > Menu.TERMINATE.ordinal());
        return Menu.MenuAt(key);
    }

    public static void main(String[] args) {
        Menu menu;                // 메뉴 
        Data data;                // 추가용 데이터 참조
        Data ptr;                 // 검색용 데이터 참조
        Data temp = new Data();   // 읽어 들일 데이터

        ArrayLinkedList<Data> list = new ArrayLinkedList<Data>(100);

        do {
            switch (menu = SelectMenu()) {

             case ADD_FIRST :                           // 머리 노드 삽입
                    data = new Data();
                     data.scanData("머리에 삽입", Data.NO | Data.NAME);
                    list.addFirst(data);
                     break;

             case ADD_LAST :                           // 꼬리 노드 삽입
                    data = new Data();
                     data.scanData("꼬리에 삽입", Data.NO | Data.NAME);
                     list.addLast(data);
                     break;

             case RMV_FIRST :                          // 머리 노드 삭제
                    list.removeFirst();
                    break;

             case RMV_LAST :                           // 꼬리 노드 삭제
                    list.removeLast();
                    break;

             case RMV_CRNT :                           // 선택 노드 삭제
                    list.removeCurrentNode();
                    break;

             case SEARCH_NO :                          // 회원번호 검색
                     temp.scanData("검색", Data.NO);
                    ptr = list.search(temp, Data.NO_ORDER);
                    if (ptr == null)
                        System.out.println("그 번호의 데이터가 없습니다.");
                    else
                        System.out.println("검색 성공 : " + ptr);
                     break;

             case SEARCH_NAME :                       // 이름 검색
                     temp.scanData("검색", Data.NAME);
                    ptr = list.search(temp, Data.NAME_ORDER);
                    if (ptr == null)
                        System.out.println("그 이름의 데이터가 없습니다.");
                    else
                        System.out.println("검색 성공 : " + ptr);
                     break;

             case NEXT :                                   // 선택 노드를 뒤쪽으로 진행
                    list.next();
                     break;

             case PRINT_CRNT :                            // 선택 노드의 데이터를 표시
                    list.printCurrentNode();
                     break;

             case DUMP :                                  // 모든 데이터를 리스트 순서로 표시
                    list.dump();
                     break;

             case CLEAR :                                 // 전체노드 삭제
                    list.clear();
                     break;
            }
        } while (menu != Menu.TERMINATE);
    }
}

 

 

이중 연결 리스트(Doubly Linked List)

 

노드

class Node<E>
{
    E data;
    Node<E> prev;
    Node<E> next;

    Node(E data)
    {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

 

노드 추가

//리스트 앞쪽에 추가
public void add(E data)
{
    Node<E> newNode = new Node(data);
    Node<E> currentNode = head;
    if(currentNode == null)
    {
        head = newNode;
        tail = newNode;
    }
    else
    {
        tail.next = newNode;
        newNode.prev = tail;
        tail = newNode;
    }
}

//리스트 뒷쪽에 추가
public void add(E data, boolean back)
{
    if(back == false) add(data);
    else
    {
        Node<E> newNode = new Node(data);
        Node<E> currentNode = tail;
        if(currentNode == null)
        {
            head = newNode;
            tail = newNode;
        }
        else
        {
            head.prev = newNode;
            newNode.prev = null;
            head = newNode;
        }
    }
}

 

노드 삽입

//원하는 인덱스에 노드 삽입
public void insert(E data, int idx)
{
    Node<E> newNode = new Node(data);
    if(head == null) add(data);
    if (head == null || idx == 0)
    {
        newNode.next = head;
        if (head != null) head.prev = newNode;
        head = newNode;
        if (tail == null) tail = newNode;
    }
    else
    {
        Node<E> currentNode = head;
        for(int i = 0; i < idx - 1; ++i)
        {
            if(currentNode == null)
            {
                System.out.println("Error");
                return;
            }
            currentNode = currentNode.next;
        }
        if (currentNode == null)
        {
            System.out.println("Invalid index");
            return;
        }
        newNode.next = currentNode.next;
        if (currentNode.next != null) currentNode.next.prev = newNode;
        newNode.prev = currentNode;
        currentNode.next = newNode;
        if(newNode.next == null) tail = newNode;
    }
}

 

노드 삭제

//노드를 앞 또는 뒷부분 삭제
public void delete(boolean back)
{
    if (head == null) System.out.println("Empty");
    else if(back == false) //앞부분 삭제
    {
        head = head.next;
        if (head != null) head.prev = null;
        else tail = null; // 리스트에 하나의 노드만 있었고 삭제한 경우
    }
    else //뒷부분 삭제
    {
        tail = tail.prev;
        if (tail != null) tail.next = null;
        else head = null; // 리스트에 하나의 노드만 있었고 삭제한 경우
    }
}

//특정 데이터의 내용을 포함하는 노드 삭제
public void delete(E data)
{
    if (head == null) System.out.println("Empty");
    else
    {
        Node<E> currentNode = head;
        while(currentNode.next != null && !currentNode.next.data.equals(data))
        {
            currentNode = currentNode.next;
        }
        if(currentNode.next != null)
        {
            currentNode.next = currentNode.next.next;
            currentNode.next.next.prev = currentNode;
        }
    }
}

 

노드 검색

//해당 데이터가 존재하는지 아닌지 확인
public void search(E data)
{
    if (head == null)
    {
        System.out.println("Invalid");
        return;
    }
    else
    {
        Node<E> currentHead = head;
        while (currentHead != null && !currentHead.data.equals(data))
        {
            currentHead = currentHead.next;
        }
        if (currentHead == null)  System.out.println("Invalid");
        else System.out.println("Valid");
    }
}

 

노드 출력

//리스트를 앞쪽부터 출력
public void printList()
{
    Node<E> currentNode = head;
    while(currentNode != null)
    {
        System.out.print(currentNode.data + " ");
        currentNode = currentNode.next;
    }
    System.out.println();
}

//리스트를 뒷쪽부터 출력
public void printList(boolean back)
{
    if(back == false) printList();
    else
    {
        Node<E> currentNode = tail;
        while(currentNode != null)
        {
            System.out.print(currentNode.data + " ");
            currentNode = currentNode.prev;
        }
        System.out.println();
    }
}

 

전체 코드

public class DoublyLinkedList<E>
{
    class Node<E>
    {
        E data;
        Node<E> prev;
        Node<E> next;

        Node(E data)
        {
            this.data = data;
            this.prev = null;
            this.next = null;
        }
    }

    private Node<E> head;
    private Node<E> tail;
    DoublyLinkedList()
    {
        this.head = null;
        this.tail = null;
    }

    //리스트를 앞쪽부터 출력
    public void printList()
    {
        Node<E> currentNode = head;
        while(currentNode != null)
        {
            System.out.print(currentNode.data + " ");
            currentNode = currentNode.next;
        }
        System.out.println();
    }

    //리스트를 뒷쪽부터 출력
    public void printList(boolean back)
    {
        if(back == false) printList();
        else
        {
            Node<E> currentNode = tail;
            while(currentNode != null)
            {
                System.out.print(currentNode.data + " ");
                currentNode = currentNode.prev;
            }
            System.out.println();
        }
    }

    //리스트 앞쪽에 추가
    public void add(E data)
    {
        Node<E> newNode = new Node(data);
        Node<E> currentNode = head;
        if(currentNode == null)
        {
            head = newNode;
            tail = newNode;
        }
        else
        {
            tail.next = newNode;
            newNode.prev = tail;
            tail = newNode;
        }
    }

    //리스트 뒷쪽에 추가
    public void add(E data, boolean back)
    {
        if(back == false) add(data);
        else
        {
            Node<E> newNode = new Node(data);
            Node<E> currentNode = tail;
            if(currentNode == null)
            {
                head = newNode;
                tail = newNode;
            }
            else
            {
                head.prev = newNode;
                newNode.prev = null;
                head = newNode;
            }
        }
    }

    //원하는 인덱스에 노드 삽입
    public void insert(E data, int idx)
    {
        Node<E> newNode = new Node(data);
        if(head == null) add(data);
        if (head == null || idx == 0)
        {
            newNode.next = head;
            if (head != null) head.prev = newNode;
            head = newNode;
            if (tail == null) tail = newNode;
        }
        else
        {
            Node<E> currentNode = head;
            for(int i = 0; i < idx - 1; ++i)
            {
                if(currentNode == null)
                {
                    System.out.println("Error");
                    return;
                }
                currentNode = currentNode.next;
            }
            if (currentNode == null)
            {
                System.out.println("Invalid index");
                return;
            }
            newNode.next = currentNode.next;
            if (currentNode.next != null) currentNode.next.prev = newNode;
            newNode.prev = currentNode;
            currentNode.next = newNode;
            if(newNode.next == null) tail = newNode;
        }
    }

    //노드를 앞 또는 뒷부분 삭제
    public void delete(boolean back)
    {
        if (head == null) System.out.println("Empty");
        else if(back == false) //앞부분 삭제
        {
            head = head.next;
            if (head != null) head.prev = null;
            else tail = null; // 리스트에 하나의 노드만 있었고 삭제한 경우
        }
        else //뒷부분 삭제
        {
            tail = tail.prev;
            if (tail != null) tail.next = null;
            else head = null; // 리스트에 하나의 노드만 있었고 삭제한 경우
        }
    }

    //특정 데이터의 내용을 포함하는 노드 삭제
    public void delete(E data)
    {
        if (head == null) System.out.println("Empty");
        else
        {
            Node<E> currentNode = head;
            while(currentNode.next != null && !currentNode.next.data.equals(data))
            {
                currentNode = currentNode.next;
            }
            if(currentNode.next != null)
            {
                currentNode.next = currentNode.next.next;
                currentNode.next.next.prev = currentNode;
            }
        }
    }

    //해당 데이터가 존재하는지 아닌지 확인
    public void search(E data)
    {
        if (head == null)
        {
            System.out.println("Invalid");
            return;
        }
        else
        {
            Node<E> currentHead = head;
            while (currentHead != null && !currentHead.data.equals(data))
            {
                currentHead = currentHead.next;
            }
            if (currentHead == null)  System.out.println("Invalid");
            else System.out.println("Valid");
        }
    }

    public static void main(String args[])
    {
        DoublyLinkedList dll = new DoublyLinkedList<>();
        dll.add(1);
        dll.add(2);
        dll.add(3);
        dll.add(4);
        dll.insert(0,0);
        dll.insert(0,2);
        dll.insert(0,4);
        dll.insert(0,6);
        dll.printList();
        dll.delete(false);
        dll.printList();
        dll.delete(true);
        dll.printList();
        dll.search(9);
    }
}

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


비즈니스 요구사항 정리

  1. 데이터 : 회원 ID, 이름
  2. 기능 : 회원 등록(단, 중복 이름 허용X), 회원 조회
  3. 아직 DB가 선정되지 않은 상황 (가상의 시나리오)

 

일반적인 웹 애플리케이션 계층 구조

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현 (예를 들어 회원 중복 가입 허용 안됨과 같은 로직들 또한 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 객체)
  • 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리 (아직 DB가 선정되지 않은 상태로 가정)
  • 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

 

클래스 의존 관계

MemberService 클래스

  • 회원 비즈니스 로직이 있는 회원 서비스. (서비스 클래스에 핵심 비즈니스 모델이 있음)

 

MemberRepository 인터페이스

  • DB에 저장하는 것이 아닌 메모리로 단순하게 저장하는 방식을 사용
  •  아직 어떤 DB를 사용할지 정하지 않은 상태이므로 인터페이스를 사용

 

MemoryMemberRepository

  • MemberRepository 인터페이스의 구현 클래스

 

 

회원 도메인과 리포지토리 만들기

회원 객체

package hello.hellospring.domain;

public class Member {

    private Long id;
    private String name;
    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;
    }
}

 

회원 리포지토리 인터페이스

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}
  • Save : member에 들어갈 정보(id, name)을 받았을 때, 이를 저장하는 기능
  • findById : id를 받아서 해당 id를 갖고 있는 member를 찾는 기능
  • findByName  : 사용자 이름을 받아서, 해당 이름을 갖고 있는 member를 찾는 기능
  • findAll : 지금까지 저장된 모든 회원 리스트를 반환하는 기능
java.util.Optional
객체를 포장해주는 Wrapper Class이다. 주로 null을 다룰 때 유용하게 사용된다.
이 클래스는 값이 존재할 수도 있고 아닐 수도 있는 컨테이너 객체를 나타낸다.
주로 메서드 반환 값으로 사용되어, 해당 메서드가 어떤 값이나 null을 반환할 수 있는 경우, null 대신에 Optional을 사용하여 더 안전한 코드를 작성할 수 있다.
Optional은 주로 메서드 체이닝과 함께 사용되어 null 처리를 더 간결하고 안전하게 할 수 있도록 도와준다.

 

회원 리포지토리 인터페이스 구현 클래스

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    public void clearStore() {
        store.clear();
    }
}

 

Hashmap.values()
HashMap 클래스의 values() 메서드는 해당 HashMap에 저장된 모든 값들을 Collection 형태로 반환한다.
이 메서드는 HashMap에 저장된 값들의 컬렉션을 얻고자 할 때 사용된다.

 

ofNullable(), filter(), findAny()

Optional.ofNullable()
주어진 값으로 Optional 객체를 생성한다. 값이 null인 경우에도 사용할 수 있다.
Optional.ofNullable 메서드의 반환 값은 Optional 클래스의 인스턴스이다.
-값이 null이 아닌 경우: Optional 인스턴스는 해당 값을 갖는다.
-값이 null인 경우: Optional 인스턴스는 빈 상태(값이 없는 상태)이다.

Optional.filter()
이 메서드는 Optional 객체 내부의 값을 조건적으로 필터링하는 데 사용된다.
이 메서드는 true 또는 false를 반환하는 test 메서드를 인자로 받는다.
Optional 내부의 값이 주어진 조건을 만족하면 동일한 값을 포함하는 Optional을 반환하고, 그렇지 않으면 빈 Optional을 반환한다.
// filter를 사용하여 값이 "World"를 포함하는지 확인
Optional<String> filteredOptional = optionalValue.filter(s -> s.contains("World"));​


Optional.findAny()
Stream 에서 첫 번째 요소를 찾아서 Optional 타입으로 리턴한다.
조건에 일치하는 요소가 없다면 empty 가 리턴된다.
따라서 Stream 의 첫 번째 요소를 구체적으로 원할 때 이 방법을 사용
(병렬 작업을 처리하는 경우 리턴값을 안정적으로 반환)

 

 

회원 리포지토리 테스트 케이스

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해
서 해당 기능을 실행한다.

이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번 에 실행하기 어렵다는 단점이 있다.

자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

보통 테스트 코드를 작성할 때, 테스트 대상이 되는 것들의 이름은 그대로 따라가고 class의 경우 뒤에 Test를 붙여주는 것이 관례이다.

 

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;

import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();
    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    @Test
    public void save() {
        //given
        Member member = new Member();
        member.setName("spring");
        //when
        repository.save(member);
        //then
        Member result = repository.findById(member.getId()).get();

        assertThat(result).isEqualTo(member);
    }
    @Test
    public void findByName() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1); Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        Member result = repository.findByName("spring1").get();
        //then
        assertThat(result).isEqualTo(member1);
    }
    @Test
    public void findAll() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);
        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);
        //when
        List<Member> result = repository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
    }
}
@AfterEach
@AfterEach는 JUnit 5에서 제공하는 어노테이션 중 하나로, 각각의 테스트 메서드가 실행된 후에 어떠한 정리 작업을 수행할 수 있도록 하는 어노테이션이다.
이 어노테이션을 사용하면 테스트 메서드가 실행된 후에 자원을 해제하거나 정리 작업을 수행하는 등의 후처리 작업을 할 수 있다.
여기서는 메모리 DB에 저장된 데이터를 삭제한다.

@BeforeEach
@BeforeEach는 JUnit 5에서 제공하는 어노테이션 중 하나로, 각각의 테스트 메서드가 실행되기 전에 필요한 초기화 작업을 수행할 수 있도록 하는 어노테이션이다.
이 어노테이션을 사용하면 중복 코드를 방지하고 테스트 메서드 간의 독립성을 유지할 수 있다.

테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다

 

Optional.get()
이 메서드는 Optional 클래스에서 값을 가져오는 메서드이다.
Optional은 값이 존재할 수도 있고 아닐 수도 있는 컨테이너로, get() 메서드는 이 컨테이너에서 값을 추출한다.
get() 메서드는 해당 Optional 객체가 값(Non-null 값)을 포함하고 있을 경우 그 값을 반환다.
// 값이 있는 Optional
Optional<String> optionalWithValue = Optional.of("Hello, World!");

// 값이 있는 경우 get() 메서드로 값을 얻음
String value = optionalWithValue.get();
System.out.println(value); // 출력: Hello, World!

// 값이 없는 Optional
Optional<String> optionalWithoutValue = Optional.empty();

// 값이 없는 경우 get() 메서드를 사용하면 NoSuchElementException 발생
// 주의: 이 부분은 예제이며, 실제 코드에서는 isPresent()나 ifPresent() 등을 사용하여 안전하게 처리하는 것이 좋습니다.
String valueFromEmptyOptional = optionalWithoutValue.get(); // 예외 발생​

그러나 get() 메서드를 사용할 때 주의해야 한다. 만약 Optional이 비어있는 경우(값이 존재하지 않는 경우), NoSuchElementException이 발생할 수 있다.
따라서 get()을 호출하기 전에 isPresent() 메서드로 값의 존재 여부를 확인하는 것이 좋다.

assert란 JUnit에서 테스트에 넣을 수 있는 정적 메서드 호출이다. 어떤 조건이 참인지 검증하며 테스트 케이스 수행 결과를 판별하는 역할을 한다.

assertEquals()
예상 값과 실제 값을 비교하여 두 값이 동일한지를 확인하는 데 사용됩니다. 이 메서드는 테스트 케이스의 성공 여부를 판단하는 데 주로 활용
String expected = "Hello, World!";
String actual = "Hello, World!";
assertEquals(expected, actual);


assertThat().isEqualTo()
메서드 체이닝을 통해 객체나 값의 동등성을 검사하는 방법이다.
이것은 테스트 코드에서 예상 값과 실제 값을 비교하고, 두 값이 동일한지 확인하는 데 사용된다.
assertThat(actual).isEqualTo(expected);

 

 

회원 서비스 개발

회원 서비스는 회원 리포지트리와 도메인을 활용해서 비지니스 로직을 작성하는 것이다.

package hello.hellospring.repository;
import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;
public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    /**
     * 회원가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
ifPresent()
Optional 클래스에서 제공하는 메서드 중 하나로, 값이 존재하는 경우에만 주어진 동작을 실행한다.
이 메서드는
Optional에 값이 존재하는지 여부를 확인하고, 값이 존재하는 경우에만 주어진 동작(Consumer)을 실행다.
// 값이 있는 Optional
Optional<String> nonEmptyOptional = Optional.of("Hello, World!");

// 값이 있는 경우 동작 실행
nonEmptyOptional.ifPresent(value -> {
    System.out.println("Value is present: " + value);
    // 여기서 필요한 다양한 동작을 수행할 수 있습니다.
});

// 값이 없는 Optional
Optional<String> emptyOptional = Optional.empty();

// 값이 없는 경우 동작 실행되지 않음
emptyOptional.ifPresent(value -> {
    System.out.println("This won't be printed, as the value is absent.");
});

 

회원 서비스 테스트

given-when-then 패턴은 test code 작성 시 자주 사용하는 패턴이다.
테스트 코드의 구조를 생각해보면, 어떤 상황이 주어져서(given), 이것을 실행했을 때(when), 어떤 결과가 나와야 한다(then)이라는 구조를 가지고 있음을 알 수 있다.


테스트 코드 작성 시, 준비 - 실행 - 검증 세 부분으로 나누어서 작성하면, 테스트 코드가 길어졌을 때 각 부분(given, when, then)을 보면 테스트 코드를 이해하는데에 도움이 된다.

Given

  • 테스트를 위해 준비를 하는 과정이다.
  • 테스트에서 사용되는 변수, 입력 값들을 정의하거나, Mock 객체를 정의하는 부분이 이에 해당된다.

When

  • 실제로 action을 하는 테스트를 실행하는 과정이다.
  • 하나의 메서드만 수행하는 것이 바람직하다. When은 대체로 가장 중요하지만 가장 짧다.

Then

  • When에서 실행한 결과를 검증하는 과정이다. 예상 값(expected)와 실제 값(actual)을 비교한다.
  • 주로 assertThat 구문을 사용하여 검증한다

 

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository); }
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }
    @Test
    public void 중복_회원_예외() throws Exception {
        //Given
        Member member1 = new Member();
        member1.setName("spring");
        Member member2 = new Member();
        member2.setName("spring");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

 

assertEquals()
메서드는 JUnit에서 제공하는 단언(assertion) 메서드 중 하나로, 예상 값과 실제 값을 비교하여 두 값이 동일한지를 확인하는 데 사용됩니다. 만약 두 값이 일치하지 않으면 해당 테스트는 실패하게 됩니다.
assertEquals(expected, actual);​

 

assertThrows()
JUnit 5에서 제공하는 메서드 중 하나로, 예외가 발생하는지 여부를 테스트하는 데 사용된다.
이 메서드는 특정 예외가 발생하는지를 확인하고, 발생한 예외를 검증할 수 있다.

assertThrows() 메서드는 예외를 기대하고, 예외가 발생하면 테스트를 성공으로 간주합니다.
예외가 발생하지 않거나 다른 예외가 발생하는 경우 테스트는 실패합니다.

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

public class AssertThrowsExample {
    @Test
    void testDivisionByZero() {
        // 예외가 발생해야 하는 코드
        assertThrows(ArithmeticException.class, () -> {
            int result = 10 / 0; // 0으로 나누기는 ArithmeticException을 발생시킴
        });
    }

    @Test
    void testIndexOutOfBounds() {
        // 예외가 발생해야 하는 코드
        assertThrows(IndexOutOfBoundsException.class, () -> {
            int[] array = new int[5];
            int element = array[10]; // 배열의 범위를 벗어난 인덱스에 접근
        });
    }
}

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


 

웹을 개발하는 방법에는 크게 세 가지가 있다.

1. 정적 컨텐츠 (static contents)

서버에서 따로 가공을 거치지 않고 파일을 웹 브라우저에 그대로 보내는 방식이다.



2. MVC와 템플릿 엔진(template engine)

과거의 JSP, PHP 같은 것들이 template engine이다.

정적 컨텐츠처럼 html을 웹 브라우저에 그대로 보내는 것이 아니라, 데이터를 가공 해서 html을 동적으로 바꿔서 웹 브라우저에 보내는 일을 한다.

이런 방법을 사용하기 위해서는 Model, View, Controller가 필요한데, 이를 합쳐서 MVC라고 부른다.

정적 컨텐츠와의 차이점
html을 웹 페이지에 그대로 전달해주는 것이 정적 컨텐츠 방식의 웹 개발인 반면,
MVC와 Template engine 방식은 server에서 약간의 변형을 하여 웹에 띄우는 방식이다.

 

3. API

html과 같은 방식이 아니라 JSON이라는 데이터 구조 포맷으로 클라이언트에게 데이터를 전달하는 방식을 API 방식이라고 한다.

view나 react를 사용할 때에도 API 방식을 사용한다.

 

정적 컨텐츠

Spring boot는 정적 컨텐츠 가능을 자동으로 제공한다.

spring boot는 /static 경로에 있는 static content들을 제공한다.

IntelliJ에서 /static 경로에 hello-static.html 을 만들고 그 안에 아무 내용이나 넣어보자.

resources/static/hello-static.html

<!DOCTYPE HTML>
<html>
<head>
 <title>static content</title>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
정적 컨텐츠 입니다.
</body>
</html>



이후 localhost:8080/hello-static.html 로 들어가면 해당 내용이 그대로 올라와있는 것을 알 수 있다.



이를 통해 알 수 있는 사실은 원하는 파일을 resources/static 폴더에 넣으면 그대로 반환이 된다는 것이다.

대신 이 방법은 정적 컨텐츠 방식이기 때문에 이후 가공이 들어가지 않고 그대로 반환이 된다.

  1. 웹 브라우저에서 localhost:8080/hello-static.html에 대해서 내장 서버인 tomcat에게 요청한다.
  2. tomcat 서버는 hello-static.html에 대해서 다시 Spring에게 요청한다.
  3. Spring이 hello-static.html 요청을 받으면
    3-1 스프링 컨테이너의 Controller에게 요청을 보낸다. (즉, Controller가 우선순위를 가진다.) Controller에 hello-static을 mapping 해놓은 메서드가 있는지 확인한다.
    3-2 Controller에서 hello-static이 mapping된 메서드를 못 찾았으므로, spring은 resources/static 폴더에서 hello-static.html 파일을 찾는다. 해당 파일을 찾게되면 그 파일을 반환해준다.

 

MVC와 템플릿 엔진

대다수의 웹 개발에서는 HTML을 그대로 주는 것이 아니라 서버에서 필요에 따라 프로그래밍 하여 동적으로 바꾸는 작업이 필요하다. 

이런 작업을 원활하게 수행하기 위해서는 각각의 역할을 나눌 필요가 있다.

이때 사용되는 것이 MVC이다.

다시 말해 정적컨텐츠는 파일을 그대로 웹브라우저로 전송하는 것이라면,

MVC는 서버에서 파일을 변형하여 내려주는 방식이다.

 

Model은 데이터 집합을 전달해주는 역할을 수행하고,

View는 화면을 그리는 역할만 수행하고,

Controller는 내부의 비지니스 로직이나 서버단의 일을 처리만 수행한다.

이런 구조를 MVC 구조라고 부른다.

@Controller
public class HelloController {
 	@GetMapping("hello-mvc")
 	public String helloMvc(@RequestParam("name") String nameValue, Model model) {
 		model.addAttribute("name", nameValue);
 		return "hello-template";
	}
}

위 코드는 HTML의 form 태그에서 name으로 전송된 값을 nameValue로 받아준다.

즉, @RequestParam을 사용함으로 인해서 name으로 전송된 값이 nameVal에 저장된 것이다.

이후 Model 객체에 addAttribute 메서드를 통해 값을 저장한다.

model.addAttribute("name", nameValue)에서 name이라는 key에 nameVal 값이 맵핑된다.

그 다음 hello-template를 반환하므로, resources/templates/hello-template.html을 찾아갈 것이다.

 

resources/templates/hello-template.html

<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>

첫째 줄을 통해 Thymeleaf를 사용한다는 것을 알 수 있다.

이 템플릿 엔진은 html을 가공해서 웹 브라우저에 전달한다.

세번째 줄을 보면 ${name} 이라는 부분을 볼 수 있는데, ${name}은 모델에서 name이라는 key에 맵핑된 value 값을 대입하게된다.

hello! empty는 만일 서버 없이 이 html 파일을 열었을 때 출력될 디폴트 값을 표시해놓은 것이다.

 

이후 http://localhost:8080/hello-mvc?name=spring 경로를 통해 실행하면 아래와 같은 화면이 나온다.

?name=spring
name이라는 key에 spring이라는 값이 담겨져서 get방식으로 데이터를 전송한다는 의미
값을 전송하지 않고 에러가 나지 않도록 하는 방법
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam(value = "name", required = false) String nameVal, Model model){
    model.addAttribute("name", nameVal);
    return "hello-template";
}

 "required = false"를 추가적으로 작성해주면 된다.
이 파라미터는 디폴트값이 true이고, true라면 key 값을 무조건 받아야한다.
하지만 false로 바꿔줌으로써 key 값을 받지 않아도 되는 것이다.
key 값을 받지 않았으므로 이 부분엔 null이 처리된다.


또는 값이 존재하지 않을 때, 기본값으로 설정한 값이 name에 들어가도록할 수 있다.

@GetMapping("hello-mvc")
public String helloMvc(@RequestParam(value = "name", required = false, defaultValue = "ReBugs")
                       String nameValue, Model model){
	model.addAttribute("name", nameValue);
	return "hello-template";
}

 

  1. 웹 브라우저에서 localhost:8080/hello-mvc 를 요청한다.
  2. spring boot를 띄울 때 같이 띄우는 내장 tomcat 서버를 먼저 거친다. tomcat 서버는 hello-mvc 요청이 왔음을 spring 컨테이너로 넘긴다.
  3. spring 컨테이너는 helloController에 hello-mvc가 @GetMapping을 통해 hello-mvc가 mapping 되어 있는 것을 확인하고, mapping 되어있는 해당 메서드를 실행한다.
    해당 메서드는 "hello-template"라는 String을 반환하고, model에는 (key : name, value : spring)이라는 값을 넣는다.
    그리고 이 값들을 viewResolver에게 넘겨준다.
  4. viewResolver가 동작한다.
    viewResolver는 view를 찾아주고, 템플릿 엔진을 연결시켜주는 역할을 한다.
    viewResolver가 templates/hello-template.html 을 찾아서 Thymeleaf 템플릿 엔진에게 처리 요청을 한다.
  5. 템플릿 엔진인 Thymeleaf가 렌더링해서 html로 변환한 후, 웹 브라우저에 반환한다.

정적 컨텐츠를 사용할 때에는 템플릿 엔진을 거치지 않고 바로 웹 브라우저에 넘겼지만, MVC 패턴은 템플릿 엔진을 통해 한 차례 변환이 된다.

 

API

spring 개발 시 이야기하는 API방식은, JSON 형식으로 바꾸어 반환하는 것을 말한다.

view없이 그대로 http body에 전달하는 방식이다.

@ResponseBody
@Controller
public class HelloController {
 	@GetMapping("hello-string")
 	@ResponseBody
 	public String helloString(@RequestParam("name") String name) {
 		return "hello " + name;
	}
}

http(통신 프로토콜)의 body 부에 return(반환)값을 직접 넣어주겠다는 뜻.
API를 사용하면 view가 따로 없다.
따라서 소스코드 보기를 통해 보아도 html 관련 코드는 없고, 출력된 문자만 보인다.
Template engine은 view라는 템플릿이 있는 상황에서 view를 html로 변환하는 것이고, API는 데이터를 그대로 내보내는 것이다.
즉, @ResponseBody 를 사용하면 뷰 리졸버( viewResolver )를 사용하지 않고, 대신에 HTTP의 BODY에 문자 내용을 직접 반환한다

 

@ResponseBody를 통해서 문자열(String)을 전달하는 경우는 거의 없고, 데이터. 즉, 객체를 요청할 때 주로 사용한다.

@Controller
public class HelloController {
    @GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String nameValue) {
        Hello hello = new Hello();
        hello.setName(nameValue);
        return hello;
    }
    static class Hello {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}
인텔리제이에서 getter와 setter 를 생성하는 단축키는 alt + insert (윈도우 기준)

이 코드를 실행하면 "/hello-api" 경로로 GET 방식으로 요청이 올 때, 요청에 포함된 "name" 파라미터를 이용하여 Hello 객체를 생성하고 이를 JSON 형태로 응답으로 반환다.

이러한 방식으로 클라이언트는 JSON 형태의 응답을 받게 된다.

<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>

즉, @RequestParam을 통해 변수 nameValue를 받은 후, 해당 값을 hello 객체의 name 변수에 받은 값을 넣어준다. 그리고 hello 객체를 반환한다.

이때 JSON 형태로 변환되는 이유는 @ResponseBody에 의해 HttpMessageConverter가 실행되고, HttpMessageConverter 중에서 JsonConvert가 실행되기 때문이다.(기본 값)

 

  1. 웹 브라우저에서 localhost:8080/hello-api를 요청하면 tomcat 서버에서 hello-api 요청이 들어왔음을 Spring에게 알린다.
  2. Spring은 먼저 Controller에 hello-api가 맵 되어 있는지 확인한다.
    이 때, 만일 @ResponseBody가 적혀있지 않았다면, viewResolver에게 전달해서 template engine이 html로 변환한다.
  3. 메서드에 @ResponseBody 어노테이션이 있고, String을 반환하는 것이 아닌 hello 객체를 반환한다.
    따라서 기본적으로 JSON 방식으로 데이터를 만들어서 http응답에 반환한다.
  4. helloController에서 메서드가 실행되어 hello 객체를 넘기게 되면 "HttpMessageConverter"가 작동한다.
    만일 단순 문자열을 반환했다면 "StringConverter"가 동작하고, 객체를 반환했다면 "JsonConverter"가 동작한다.
    이 상황에서는 hello 객체를 받았기 때문에 JsonConverter가 동작하여 hello 객체의 내용을 JSON 형식으로 바꾼다.
  5. JSON 스타일로 바꾼 내용을 요청한 웹 브라우저/서버에게 응답한다.
정리
@ResponseBody 를 사용시 아래의 처리가 이뤄짐
HTTP의 BODY에 문자 내용을 직접 반환
viewResolver 대신에 HttpMessageConverter 가 동작
 - 기본 문자처리: StringHttpMessageConverter
 - 기본 객체처리: MappingJackson2HttpMessageConverter
 - byte 처리등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


라이브러리 살펴보기

External Libraries

Project 창에서 "External Libraries"를 찾을 수 있는데, 이 External Libraries를 보면 가져온 라이브러리들이 무엇인지 볼 수 있다.

해당 폴더를 펼치면 어떤 라이브러리들이 포함되어 있는지 알 수 있다.

하지만 항목들이 너무 많기 때문에 뭐가 뭔지 알기가 힘들다.

이럴 때에는 Gradle 탭을 활용하면 좋다.

 

Gradle 탭

Gradle 탭의 Dependencies를 보면 선택한 라이브러리를 볼 수 있다.

Denpendencies는 의존성이라는 뜻인데, gradle(maven)의 의존 관계를 관리해준다.

Dependencies 항목을 열어보면 각 항목이 있는 것을 볼 수 있는데, 다시 그 항목을 열어보면 다른 항목들이 있는 것을 볼 수 있다.

상위 항목이 하위 항목에 의존한다.

즉, 필요로 한다라는 뜻이다.

예를 들어 spring-boot-starter-web을 열어보면 다음과 같이 다른 항목들이 있음을 볼 수 있다.

spring-boot-starter-web을 보면 밑에 json, tomcat, spring-boot-starter, spring-web, spring-webmvc가 있다.

이는 spring-boot-starter-web을 사용하기 위해서는 위 항목들이 필요하다는 뜻이다.

이런 식으로 해당 기능을 사용하기 위해 다른 어떤 기능들이 필요한지 계층 구조로 보여준다.



중요한 항목은 아래와 같다.

  1. spring-boot-starter-web을 열어보면 하위에 tomcat이 있다. tomcat은 웹 서버에 내장되어 있다.
    이전에는 웹 서버에 tomcat을 직접 설치했어야 했지만 현재는 따로 설정을 하지 않아도 된다.
  2. spring boot starter web -> json -> web 에 들어가면 spring-core 또한 가져오고 있음을 알 수 있다.

 

Log

Java에서 텍스트를 출력할 때에 System.out.println 을 사용하지만, 실무에서는 저 함수를 사용하는 것이 아니라 Log로 출력해야 한다.

그 이유는 Log 파일을 따로 만들어야 심각한 에러들을 따로 모아보는 식으로 분류를 하여 로그 파일들을 관리할 수 있기 때문이다.

Log를 편하게 해주는 라이브러리
spring-boot-starter-thymeleaf를 열어보면 spring-boot-starter-logging이 있다.
다시 이 항목을 열어보면 slf4j와 logback이라는 항목이 있다.
slf4j는 인터페이스이고, 실제 로그를 어떤 구현체로 출력할까는 logback으로 선택한다. logback이 성능도 빠르고 지원하는 것들이 많다.
이 두 항목이 Log를 좀 더 편하게 사용할 수 있도록 도와준다.

 

test와 관련된 라이브러리

test와 관련된 라이브러리들도 확인할 수 있다.

java에서 많이 사용하는 junit을 확인할 수 있는데, spring은 java기반이기 때문에 spring 또한 junit을 많이 사용한다.

mockito, assertj도 있는데, 이 항목들은 test를  편리하게 해주는 라이브러리들이다.

spring-test 항목도 확인할 수 있는데, 이 라이브러리는 spring과 통합해서 test할 수 있게 해주는 라이브러리이다.

핵심은 junit 라이브러리이다.


View 환경설정

Welcome page 만들기

spring boot는 resources/static/index.html 파일을 넣으면 해당 파일을 welcome page로 만들어준다.

main - resources - static 폴더에 오른쪽 클릭을 하고 new File을 눌러서 indexl.html을 만들어주자.

그럼 이 페이지가 welcome page가 될 것이다.

이후 index.html에 내용을 적고 서버를 실행하면 해당 화면으로 바뀔 것이다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/hello">hello</a>
</body>
</html>
welcome page에 대해 좀 더 알고 싶다면 spring boot reference doc을 참고
https://docs.spring.io/spring-boot/docs/3.2.1/reference/htmlsingle/

 

Thymeleaf 템플릿 엔진(Template engine)

위에서 만든 것은 "정적 페이지 : static"이다. 

html에서 적어놓은 파일을 웹 브라우저에 그대로 넘기는 것이다. 파일을 그대로 넘긴 것이다.

하지만 Template engine을 사용하면 본인이 원하는대로 프로그래밍을 해서 html의 모양을 바꿀 수 있다.

우리가 이번에 사용하는 템플릿 엔진은 Thymeleaf라는 템플릿 엔진인데, 버전 3으로 넘어오면서 많은 부분이 개선되었다.

spring boot features에서 Template engines를 검색하면 어떤 종류의 템플릿 엔진이 있는지 볼 수 있다.

 

Controller와 Model을 이용하여 화면 만들기

Controller와 Model을 이용하여 동작하는 화면을 만들어보자.

1. 먼저 java - Practice1.practicespring 밑에 controller package를 하나 만든다.

그리고 HelloController 클래스를 하나 생성한다.

2. HelloController에 @Controller라는 어노테이션을 적어 spring에서 컨트롤러임을 인식하도록 한다.

해당 클래스 밑에 helloFunction이라는 메서드를 하나 생성하고, @GetMapping("hello")를 적어준다.

@GetMapping("hello") 어노테이션을 적게 되면 웹 어플리케이션 url에서 /hello가 붙게 되면 spring에서 해당 메서드를 호출한다.

package hello.hellospring.controller;

@Controller
public class HelloController {
    @GetMapping("hello")
    public String hello(Model model){
    
    }
}

 

3. hello() 를 정의해주자. 여기서 매개변수로 전달해준 Model은 MVC 패턴에서의 Model이다.

addAttribute를 통해 값을 추가해준다.

여기서 눈 여겨 볼 사항은 문자열 "hello"를 반환한 것인데, 간단하게 말하자면 resources/templates의 hello.html을 찾으라는 뜻이다.

package hello.hellospring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloController {
    @GetMapping("hello")
    public String hello(Model model){
        model.addAttribute("data", "spring");
        return "hello";
    }
}

 

4. resources/templates에서 New File을 눌러 hello.html을 생성하고 다음과 같이 작성하자.



<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>

두 번째 줄의 링크는 thymeleaf를 스키마로 선언한 것이며, 이를 통해 thymeleaf 템플릿 엔진의 문법을 사용할 수 있게 된다.

또한 중간 중간 보이는 th는 thymeleaf의 약자이다.

${data}를 볼 수 있는데, 이는 model 객체에 있는 data라는 key 값에 해당하는 value값으로 대입된다.

이후 intelliJ로 빌드를 하고, herf로 되어있는 hello를 누르면 "안녕하세요, hello!!"가 화면에 뜰 것이다.

 

 동작 환경 그림

1. 웹 브라우저에서 localhost:8080/hello 를 요청하면 spring boot에 내장되어있는 웹 서버인 tomcat에게 요청 사항을 전달한다. tomcat은 다시 spring에게 hello 요청을 전달한다.

 

2. spring은 우선 controller인 helloController에 @GetMapping("hello")로 맵핑 되어 있는 함수 hello()를 찾아서 실행한다.

 

해당 함수에서는 Model이 매개변수로 들어오는데, spring이 Model 객체에 key는 data이고, value는 hello!!인 attribute를 추가한다. 그리고 "hello"를 반환한다.

 

3. Controller에서 return 값으로 string을 반환하면, viewResolver가 화면(view)를 찾아서 처리를 해준다.

이 때, view mapping이 이루어지는데, view mapping이란 resources/templates/(ViewName).html 경로로 되어있는 파일을 찾는데 여기서 ViewName을 return 값인 hello로 대입하는 것을 말한다.

 

4. Thymeleaf 템플릿 엔진이 viewResolver가 찾아준 hello.html을 렌더링하여 웹 브라우저에 띄운다.

hello.html에 ${data}에는 Controller의 hello()에 있는 model 객체에 추가한 attribute의 key값이다.

실제로 이 view를 렌더링 할 때에는 key에 해당하는 value 값. 즉 hello!!로 렌더링이 된다.

 

spring-boot-devtools 라이브러리 추가
이 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이 View 파일 변경이 가능하다.

1. 의존성 추가 (build.gradle 수정)
dependencies {
	implementation 'org.springframework.boot:spring-boot-devtools'
}​


2. IntelliJ 설정
1) Setting > Build, Exeution, Deployment > Compiler > Build project autiomaically 체크


2) Advanced Settings > Allow auto-make to start even if developed application is currently running 체크


3. Reload Gradle Changes 클릭


이 설정이 처음이라면 인텔리제이를 재시작한다.

 

빌드하고 실행하기

윈도우 개발환경 기준

 

프로젝트 파일은 D:\Programming\intelij\Spring\hello-spring\hello-spring 경로에 있다.

따라서 cmd에서 이 경로로 이동해야한다.

D드라이브에 프로젝트 폴더가 있기 때문에 D 드라이브로 이동한다.

 

gradlew build 명령어를 이용하여 빌드를 한다.

build 폴더가 생긴것을 확인할 수 있다.

 

이제 실행을 하려면 아래의 과정을 거치면 된다.

bulid 폴더로 이동하고, 그 안에 있는 libs 폴더로 이동한다.

그 다음 dir 명령어를 통해 폴더 내부에 무엇이 있는지 살펴본다.

-plain 이 붙은 파일이 아닌 jar 파일을 실행한다.

java -jar hello-spring-0.0.1-SNAPSHOT.jar

이러면 정상적으로 실행이 된 것이다.

 

빌드 삭제하기
다시 D:\Programming\intelij\Spring\hello-spring\hello-spring 경로로 이동해야 한다.

이후 gradlew clean 명령어를 입력하면 build 폴더 통째로 사라진다.

이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다.


클래스 기본 형태

다른 프로그래밍 언어와 비슷한 형태이다.

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      class Student {
        //생성자
        constructor (이름, 국어, 영어, 수학, 과학) {
          this.이름 = 이름
          this.국어 = 국어
          this.영어 = 영어
          this.수학 = 수학
          this.과학 = 과학
        }
		//메소드 -> 메소드는 콜백함수를 사용할 수 없음
        getSum () {
          return this.국어 + this.영어 + this.수학 + this.과학
        }
        getAverage () {
          return this.getSum() / 4
        }
        toString () {
          return `${this.이름}\t${this.getSum()}점\t${this.getAverage()}점\n`
        }
      }
      // 객체를 선언합니다.
      const students = []
      students.push(new Student('구름', 87, 98, 88, 90))
      students.push(new Student('별이', 92, 98, 96, 88))
      students.push(new Student('겨울', 76, 96, 94, 86))
      students.push(new Student('바다', 98, 52, 98, 92))

      //출력합니다.
      let output = '이름\t총점\t평균\n'
      for (const s of students) {
        output += s.toString()
      }
      console.log(output)
    </script>
  </head>
  <body>
  </body>
</html>

 

private

클래스 내부에서 사용되는 # 기호로 시작하는 이름으로 선언된 멤버가 private 멤버로 취급된다.

이러한 private 멤버는 클래스 외부에서 직접 접근할 수 없다.

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      // 정사각형 클래스
      class Square {
        #length //private 변수

        constructor (length) {
          this.setLength(length)
        }

        setLength (value) { /*setter*/
          if (value <= 0) {
            throw '길이는  0보다 커야 합니다.'
          }
          this.#length = value
        }

        getLength (value) { /*getter*/
          return this.#length
        }
        
        //메소드
        getPerimeter () { return 4 * this.#length }
        getArea () { return this.#length * this.#length }
      }

      // 클래스 사용하기
      const square = new Square(10)
      console.log(`한 변의 길이는 ${square.getLength()}입니다.`)

      // 예외 발생시키기
      square.setLength(-10)
    </script>
  </head>
  <body>
  </body>
</html>

 

static

JavaScript에서 static 키워드는 클래스의 정적(static) 멤버를 정의할 때 사용된다.

정적 멤버는 클래스 자체에 속하며 인스턴스를 생성하지 않고도 클래스에서 직접 접근할 수 있는 멤버이다.

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      class Square {
        #length
        static #conuter = 0 /*statc 키워드*/
        static get counter () { /*statc 메소드*/
          return Square.#conuter
        }
        constructor (length) { /*생성자*/
          this.length = length
          Square.#conuter += 1
        }

        static perimeterOf (length) {
          return length * 4
        }
        static areaOf (length) {
          return length * length
        }
        
        get length () { return this.#length }
        get perimeter () { return this.#length * 4 }
        get area () { return this.#length * this.#length }

        set length (length) {
          if (length < 10) {
            throw '길이는 0보다 커야 합니다.'
          }
          this.#length = length
        }
      }

      // static 속성 사용하기
      const squareA = new Square(10)
      const squareB = new Square(20)
      const squareC = new Square(30)
      console.log(`지금까지 생성된 Square 인스턴스는 ${Square.counter}개입니다.`)
      // static 메소드 사용하기
      console.log(`한 변의 길이가 20인 정사각형의 둘레는 ${Square.perimeterOf(20)}입니다.`)
      console.log(`한 변의 길이가 30인 정사각형의 둘레는 ${Square.areaOf(30)}입니다.`)
    </script>
  </head>
  <body>
  </body>
</html>

 

상속

JavaScript에서 클래스를 정의할 때, extends 키워드를 사용하여 다른 클래스를 상속할 수 있다.

만약 부모 클래스의 생성자(constructor)가 있는 경우, 하위 클래스(자식 클래스)에서는 super()를 호출하여 부모 클래스의 생성자를 명시적으로 호출해야 한다.

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      // 사각형 클래스
      class Rectangle {
        constructor (width, height) {
          this.width = width
          this.height = height
        }

        // 사각형의 둘레를 구하는 메소드
        getPerimeter () {
          return 2 * (this.width + this.height)
        }

        // 사각형의 넓이를 구하는 메소드
        getArea () {
          return this.width * this.height
        }
      }

      // 정사각형 클래스
      class Square extends Rectangle {
        constructor (length) {
          super(length, length)
        }

      }

      // 클래스 사용하기
      const square = new Square(10, 20)
      console.log(`정사각형의 둘레: ${square.getPerimeter()}`)
      console.log(`정사각형의 넓이: ${square.getArea()}`)
    </script>
  </head>
  <body>
  </body>
</html>

'JavaScript Category > JavaScript' 카테고리의 다른 글

[JavaScript] 예외처리  (0) 2024.01.18
[JavaScript] 문서 객체 모델(DOM:Document Object Model)  (0) 2024.01.17
[JavaScript] 객체(Object)  (0) 2024.01.16
[JavaScript] 함수  (1) 2024.01.15
[JavaScript] 반복문  (0) 2024.01.14

이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다.


try - catch - finally 문

try{
      //예외 발생 가능성이 있는 코드
    } catch(exception){
      //예외처리를 할 코드
    } finally{
      //무조건적으로 실행될 코드
    }
finally 구문은 catch 구문에서 return, break, continue 등을 만나더라도 실행된다.
또한 파일 입출력등에서 파일을 사용 도중에 오류를 만나면 리소스를 자동으로 닫는 상황에서도 사용된다.

 

예외 객체

예외 객체 속성

속성이름 설명
name 예외 이름
message 예외 메시지
<!DOCTYPE html>
<html>
<head>
  <title>localStorage에 JSON 데이터 저장하기</title>
  <script>
    try{
      console.rog("Asd") //예외 발생코드
    } catch(exception){
      console.log(exception.name)
      console.log(exception.message)
    } finally{
      //무조건적으로 실행될 코드
    }
  </script>
</head>
<body>
</body>
</html>

 

예외 강제 발생

상황에 따라서 예외를 강제로 발생시켜야 하는 경우도 있다.

자바스크립트는 유연한 언어라서 엄청나게 큰 잘못을 하지 않는이상 예외를 발생시키지 않기 때문에, 프로그램의 안정성을 위해서 예외를 강제로 발생시켜야 하는 경우가 종종 발생한다.

// 단순하게 예외를 발생
throw '예외 내용'

//조금 더 자세하게 예외 발생
throw new Error('예외 내용')

 

function divide(a, b) {
      if (b === 0) {
        throw '나누는 수는 0이 될 수 없습니다.'
      }
      return a / b;
    }
    const result = divide(10, 0);

 

function divide(a, b) {
  if (b === 0) {
    throw new Error('나누는 수는 0이 될 수 없습니다.');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log('나눈 결과:', result);
} catch (error) {
  console.error('예외 발생:', error.message);
}

 

'JavaScript Category > JavaScript' 카테고리의 다른 글

[JavaScript] 클래스  (0) 2024.01.18
[JavaScript] 문서 객체 모델(DOM:Document Object Model)  (0) 2024.01.17
[JavaScript] 객체(Object)  (0) 2024.01.16
[JavaScript] 함수  (1) 2024.01.15
[JavaScript] 반복문  (0) 2024.01.14

이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다.


문서 객체 조작

DOMContentLoaded

DOMContentLoaded는 웹 페이지의 모든 HTML 요소가 브라우저에 의해 로드되고 파싱되었을 때 발생하는 이벤트이다.

이는 이미지, 스타일시트 등의 외부 자원을 기다리지 않고, HTML 요소들 자체가 다 로드되었을 때 발생한다.

이벤트가 발생하면 JavaScript에서 이를 감지하여 특정 기능을 실행하거나 초기화 작업을 할 수 있다.

페이지의 모든 구성 요소가 준비된 상태에서 JavaScript 코드를 실행할 수 있는 타이밍을 제공하여, 더 나은 사용자 경험을 만들거나 웹 애플리케이션을 초기화하는 데 활용할 수 있다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <title>transition-timing-function</title>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // DOMContentLoaded 이벤트가 발생했을 때 실행할 코드
      console.log('DOM이 완전히 로드되었습니다!');
      
      document.body.innerHTML = '<h1>새로운 제목</h1><p>새로운 문단</p>'
    });
  </script>
</head>
<body>
  <!-- 바디 태그에선 실질적으로 입력한 것이 없음. -->
</body>
</html>

 

문서 객체 가져오기

document.body를 사용하면 문서와 body 요소를 읽어들일 수 있다.

이외에도 HTML 문서에 있는 요소와 title 요소 등은 아래와 같은 방법으로 읽어 들일 수 있다.

  • document.body
  • document.head
  • document.title

 

document.querySelector

body 요소 내부에 만든 다른 요소들은 아래와 같은 다른 별도의 메소드를 사용해서 접근한다.

document.querySelector는 CSS 선택자를 사용하여 문서에서 요소를 선택하는 JavaScript의 메서드이다.

이를 통해 문서 내에서 원하는 요소를 선택하고 조작할 수 있다.

// 클래스가 'example'인 첫 번째 <div> 요소 선택
const element = document.querySelector('div.example');

// id가 'myElement'인 요소 선택
const specificElement = document.querySelector('#myElement');

// <button> 태그 중 첫 번째 요소 선택
const button = document.querySelector('button');

// 속성 선택자를 사용하여 선택
const attributeElement = document.querySelector('[data-id="123"]');

 

<!DOCTYPE html>
<html>
<head>
  <title>querySelector 예시</title>
</head>
<body>
  <h1 id="mainHeading">안녕하세요, 여기는 제목입니다!</h1>
  <h1 id="mainHeading">안녕하세요, 여기는 제목입니다!</h1>

  <script>
    // h1 태그 선택
    const heading = document.querySelector('#mainHeading');

    // 내용 변경
    heading.textContent = '환영합니다!';

    // 스타일 변경
    heading.style.color = 'blue';
    heading.style.fontSize = '28px';
  </script>
</body>
</html>

 

document.querySelectorAll

document.querySelectorAll는 지정된 CSS 선택자에 해당하는 모든 요소를 찾아 NodeList라는 정적인 유사 배열(Collection)로 반환하는 JavaScript의 메서드이다.

// 클래스가 'example'인 모든 <div> 요소들을 선택하여 NodeList로 반환
const elements = document.querySelectorAll('div.example');

// 클래스가 'highlight'인 모든 요소들을 선택하여 NodeList로 반환
const highlightedElements = document.querySelectorAll('.highlight');

// <p> 태그 중 모든 요소들을 선택하여 NodeList로 반환
const paragraphs = document.querySelectorAll('p');

querySelectorAll은 일치하는 모든 요소를 선택하고 NodeList로 반환하기 때문에 forEach나 다른 반복문을 사용하여 NodeList의 각 요소에 접근하고 조작할 수 있다.

// 선택된 모든 <p> 요소의 텍스트 내용을 변경
const paragraphs = document.querySelectorAll('p');
paragraphs.forEach(paragraph => {
  paragraph.textContent = '새로운 내용';
});

 

<!DOCTYPE html>
<html>
<head>
  <title>querySelectorAll 예시</title>
</head>
<body>
  <h1>첫 번째 제목</h1>
  <h1>두 번째 제목</h1>
  <h1>세 번째 제목</h1>

  <script>
    // 모든 h1 태그 선택
    const headings = document.querySelectorAll('h1');

    // 각 h1 태그의 내용 변경
    headings.forEach((heading, index) => {
      heading.textContent = `새로운 제목 ${index + 1}`;
      heading.style.color = 'green';
    });
  </script>
</body>
</html>

 

글자 조작

innerHTMLtextContent는 JavaScript에서 DOM 요소의 내용을 조작하는 데 사용되는 두 가지 속성이다.

보통 텍스트만 변경해야 하는 경우에는 textContent를 사용하고, HTML을 조작해야 하는 경우에는 innerHTML을 사용한다.

그러나 innerHTML을 사용할 때는 새 HTML을 파싱하고 해당 요소의 내용을 완전히 대체하기 때문에, 기존에 연결된 이벤트 핸들러 등이 손실될 수 있다.

// 요소의 HTML 내용을 변경
element.innerHTML = '<p>새로운 HTML</p>';
// 요소의 텍스트 내용을 변경
element.textContent = '새로운 텍스트';

 

<!DOCTYPE html>
<html>
<head>
  <title>innerHTML vs textContent</title>
</head>
<body>
  <div id="exampleInnerHTML">innerHTML 예시</div>
  <div id="exampleTextContent">textContent 예시</div>

  <script>
    const innerHTMLExample = document.getElementById('exampleInnerHTML');
    innerHTMLExample.innerHTML = '새로운 innerHTML 예시';

    const textContentExample = document.getElementById('exampleTextContent');
    textContentExample.textContent = '새로운 textContent 예시';
  </script>
</body>
</html>

 

속성 조작

getAttributesetAttribute는 JavaScript에서 요소의 속성(attribute)을 가져오거나 설정하는 메서드이다.

 

getAttribute: 지정된 요소의 속성 값을 반환합니다. 이를 사용하여 요소의 속성 값을 가져올 수 있다.

// id 속성의 값을 가져옴
const elementId = element.getAttribute('id');

// href 속성의 값을 가져옴
const linkHref = linkElement.getAttribute('href');

 

setAttribute: 지정된 요소에 속성을 설정하거나 새로운 속성을 추가다.

첫 번째 인자로는 설정하려는 속성의 이름을, 두 번째 인자로는 속성의 값을 전달한다.

// id 속성을 설정
element.setAttribute('id', 'newIdValue');

// href 속성을 설정
linkElement.setAttribute('href', 'https://www.example.com');

 

스타일 조작

style 속성은 JavaScript에서 DOM 요소의 인라인 스타일을 직접 조작할 때 사용된다.

이 속성은 해당 요소의 스타일을 동적으로 변경하는 데 유용하다.

CSS와 JavaScript에서 사용되는 스타일 속성 이름은 대부분 유사하지만 몇 가지 주의해야 할 점이 있다.

하이픈(-)을 사용하는 속성 이름: CSS에서 하이픈을 사용하여 표기하는 몇 가지 속성 이름은 JavaScript에서 직접 속성 이름으로 사용할 수 없다. JavaScript에서는 카멜 케이스(camelCase)로 변환하여 사용해야 한다.

  • CSS: font-size
  • JavaScript: fontSize

 

또는 아래와 같이 스타일을 조작할 수 있다.

h1.style['background-color']

 

<!DOCTYPE html>
<html>
<head>
  <title>h1 태그 스타일 조작</title>
</head>
<body>
  <h1 id="mainHeading">안녕하세요, 여기는 제목입니다!</h1>

  <button onclick="changeHeadingStyle()">제목 스타일 변경</button>

  <script>
    function changeHeadingStyle() {
      const heading = document.getElementById('mainHeading');

      // h1 태그의 스타일 변경
      heading.style.color = 'blue';
      heading.style.fontSize = '32px';
      heading.style.fontFamily = 'Arial, sans-serif';
      heading.style.textAlign = 'center';
    }
  </script>
</body>
</html>

 

문서 객체 생성

이벤트 리스너와 이벤트 핸들러는 JavaScript에서 이벤트를 관리하고 처리하는 데 사용되는 용어이다.

  • 이벤트 리스너 (Event Listener): 이벤트 리스너는 특정 이벤트(클릭, 마우스 오버, 키 누름 등)가 발생했을 때 그것을 감지하고, 이에 대응하여 특정한 동작을 수행하는 함수이다.
  • 이벤트 핸들러 (Event Handler): 이벤트 핸들러는 이벤트가 발생했을 때 실행되는 함수를 가리킨다. 이벤트 핸들러는 이벤트 리스너에 등록되어 특정 이벤트가 발생했을 때 실행된다. 이벤트 핸들러는 주로 콜백 함수 형태로 사용된다.
// 버튼 요소 선택
const button = document.getElementById('myButton');

// 이벤트 핸들러 함수
function clickHandler() {
  alert('버튼이 클릭되었습니다!');
}

// 이벤트 리스너 추가
button.addEventListener('click', clickHandler);

문서 객체를 생성하고 싶을 때는 document.createElement()를 이용한다.

document.createElement() 메서드는 새로운 HTML 요소를 생성하는 JavaScript의 기능이다.

이 메서드를 사용하면 스크립트로 새로운 HTML 요소를 동적으로 생성할 수 있다.

// 새로운 button 요소 생성
const newButton = document.createElement('button');

// 버튼에 텍스트 추가
newButton.textContent = '클릭하세요!';

// 버튼에 이벤트 리스너 추가
newButton.addEventListener('click', function() {
  alert('버튼이 클릭되었습니다!');
});

// 버튼을 body에 추가
document.body.appendChild(newButton);

 

appendChild
appendChild는 JavaScript에서 DOM 요소에 자식 요소를 추가하는 메서드이다.
이 메서드를 사용하면 새로운 요소를 기존 요소의 자식으로 추가할 수 있다.
예를 들어, 새로운 <p> 요소를 생성하고 이를 <div> 요소의 자식으로 추가하는 코드는 다음과 같다.
// 새로운 <p> 요소 생성
const newParagraph = document.createElement('p');
newParagraph.textContent = '새로운 문장';

// <div> 요소 선택
const targetDiv = document.getElementById('targetDiv');

// <div> 요소에 새로운 <p> 요소를 자식으로 추가
targetDiv.appendChild(newParagraph);​

 

getElementById()
Document 객체의 메서드로, HTML 문서 내에서 특정 ID를 가진 요소를 가져올 때 사용된다.
이 메서드는 해당 ID를 가진 요소를 반환한다.

 

문서 객체 제거

 

일반적으로 요소를 제거하려면 해당 요소의 부모 요소로부터 제거하는 방식을 사용한다.

parentElement.removeChild(childElement);

또는

// 부모 요소 직접 참조하여 제거
const elementToRemove = document.getElementById('toBeRemoved');
elementToRemove.parentNode.removeChild(elementToRemove);

 

이벤트 설정

// 버튼 요소 선택
const button = document.getElementById('myButton');

// 버튼에 클릭 이벤트 리스너 추가
button.addEventListener('click', function() {
  // 이벤트 발생 시 실행할 코드
  alert('버튼이 클릭되었습니다!');
});

addEventListener는 이벤트 유형(예: 'click', 'mouseover' 등)과 그에 따라 실행될 함수(이벤트 핸들러)를 지정하여 요소에 이벤트를 추가할 수 있다.

이를 통해 사용자의 상호작용에 반응하여 원하는 동작을 수행할 수 있다.

 

이벤트 제거

button.removeEventListener(이벤트 이름, 핸들러 함수);
const button = document.getElementById('myButton');

function clickHandler() {
  alert('버튼이 클릭되었습니다!');
}

// 클릭 이벤트 리스너 추가
button.addEventListener('click', clickHandler);

// 클릭 이벤트 리스너 제거
button.removeEventListener('click', clickHandler);

 

 

이벤트 활용

키보드 이벤트

키보드 이벤트는 아래와 같이 3가지가 있다.

//keydown 이벤트
document.addEventListener('keydown', function(event) {
  console.log('Key pressed:', event.key);
});

//keyup 이벤트
document.addEventListener('keyup', function(event) {
  console.log('Key released:', event.key);
});

//keypress 이벤트
document.addEventListener('keypress', function(event) {
  console.log('Key pressed (keypress):', event.key);
});

keypress 이벤트는 아시아권 문자를 제대로 처리하지 못하는 오류가 있어서 보통 keyup 이벤트를 사용한다.

 

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const textarea = document.querySelector('textarea')
        const h1 = document.querySelector('h1')

        textarea.addEventListener('keyup', (event) => {
          const length = textarea.value.length
          h1.textContent = `글자 수: ${length}`
        })
      })
    </script>
  </head>
  <body>
    <h1></h1>
    <textarea></textarea>
  </body>
</html>
textarea 등과 같은 텍스트를 입력하는 양식의 값은 value 속성으로 읽어드릴 수 있다.

 

키보드 키코드 사용

키 코드를 이용하여 이벤트 핸들러에서 특정 키를 감지하고 처리할 수 있다.

하지만 주의해야 할 점이 있다.

keyCode는 Deprecated되었고, 대신에 keycode 속성을 사용하는 것이 권장된다.

key는 키의 실제 값(예: 'a', 'Enter', 'Shift' 등)을 나타내고, code는 키보드의 물리적인 위치를 나타내는 값이다.

document.addEventListener('keydown', function(event) {
  // Using key property
  console.log('Key pressed (using key):', event.key);

  // Using code property
  console.log('Key pressed (using code):', event.code);

  // Example: Check if the 'Enter' key is pressed
  if (event.key === 'Enter' || event.code === 'Enter') {
    console.log('Enter key pressed');
  }
});

 

이벤트 발생 객체

이벤트를 발생시킨 객체에 접근하기 위해서는 두 가지 방법이 있다.

  • event.currentTarget
  • this

event.currentTarget를 통해 접근

<!DOCTYPE html>
<html>
<head>
  <title>h1 태그 스타일 조작</title>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const textarea = document.querySelector('textarea');
      const h1 = document.querySelector('h1');

      const listener = (event) => {
        const length = event.currentTarget.value.length;
        h1.textContent = `글자 수 ${length}`;
      };

      textarea.addEventListener('keyup', listener);
    });
  </script>
</head>
<body>
  <h1></h1>
  <textarea></textarea>
</body>
</html>

 

 

 

 

this를 통해 접근

<!DOCTYPE html>
<html>
<head>
  <title>h1 태그 스타일 조작</title>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const textarea = document.querySelector('textarea');
      const h1 = document.querySelector('h1');

      const listener = function() {
        const length = this.value.length;
        h1.textContent = `글자 수 ${length}`;
      };

      textarea.addEventListener('keyup', listener);
    });
  </script>
</head>
<body>
  <h1></h1>
  <textarea></textarea>
</body>
</html>

 

글자 입력 양식 이벤트

inch를 cm 단위로 변환하는 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const input = document.querySelector('input')
        const button = document.querySelector('button')
        const p = document.querySelector('p')

        button.addEventListener('click', () => {
          // 입력을 숫자로 변환합니다.
          const inch = Number(input.value)
          // 숫자가 아니라면 바로 리턴합니다.
          if (isNaN(inch)) {
            p.textContent = '숫자를 입력해주세요'
            return
          }
          // 변환해서 출력합니다.
          const cm = inch * 2.54
          p.textContent = `${cm} cm`
        })
      })
    </script>
  </head>
  <body>
    <input type="text"> inch<br>
    <button>계산</button>
    <p></p>
  </body>
</html>

 

이메일 형식 확인 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const input = document.querySelector('input')
        const p = document.querySelector('p')
        const isEmail = (value) => {
          // 골뱅이를 갖고 있고 && 골뱅이 뒤에 점이 있다면
          return (value.indexOf('@') > 1)
            && (value.split('@')[1].indexOf('.') > 1)
        }

        input.addEventListener('keyup', (event) => {
          const value = event.currentTarget.value
          if (isEmail(value)) {
            p.style.color = 'green'
            p.textContent = `이메일 형식입니다: ${value}`
          } else {
            p.style.color = 'red'
            p.textContent = `이메일 형식이 아닙니다: ${value}`
          }
        })
      })
    </script>
  </head>
  <body>
    <input type="text">
    <p></p>
  </body>
</html>

change 이벤트
input.addEventListener('keyup', (event) => {​
이 코드에서 keyup 대신 change를 사용할 수 있다.
change 이벤트는 사용자가 입력 폼 요소 (주로 <input>, <select>, <textarea>)에서 내용을 변경하고 포커스를 잃었을 때 발생하는 이벤트이다.
이 이벤트는 사용자가 값을 변경하고 입력을 완료하거나 해당 요소를 떠날 때 한 번만 발생한다.

 

드롭다운 목록 활용

기본 select 태그 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const select = document.querySelector('select')
        const p = document.querySelector('p')

        select.addEventListener('change', (event) => {
          const options = event.currentTarget.options
          const index = event.currentTarget.options.selectedIndex

          p.textContent = `선택: ${options[index].textContent}`
        })
      })
    </script>
  </head>
  <body>
    <select>
      <option>떡볶이</option>
      <option>순대</option>
      <option>오뎅</option>
      <option>튀김</option>
    </select>
    <p>선택: 떡볶이</p>
  </body>
</html>

 

mutiple select 태그

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const select = document.querySelector('select')
        const p = document.querySelector('p')

        select.addEventListener('change', (event) => {
          const options = event.currentTarget.options
          const list = []
          for (const option of options) {
            if (option.selected) {
              list.push(option.textContent)
            }
          }
          p.textContent = `선택: ${list.join(',')}`
        })
      })
    </script>
  </head>
  <body>
    <select multiple>
      <option>떡볶이</option>
      <option>순대</option>
      <option>오뎅</option>
      <option>튀김</option>
    </select>
    <p></p>
  </body>
</html>

 

cm 단위를 여러 단위로 변환하는 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        let 현재값
        let 변환상수 = 10

        const select = document.querySelector('select')
        const input = document.querySelector('input')
        const span = document.querySelector('span')

        const calculate = () => {
          span.textContent = (현재값 * 변환상수).toFixed(2)
        }

        select.addEventListener('change', (event) => {
          const options = event.currentTarget.options
          const index = event.currentTarget.options.selectedIndex
          변환상수 = Number(options[index].value)
          calculate()
        })

        input.addEventListener('keyup', (event) => {
          현재값 = Number(event.currentTarget.value)
          calculate()
        })
      })
    </script>
  </head>
  <body>
    <input type="text"> cm =
    <span></span>
    <select>
      <option value="10">mm</option>
      <option value="0.01">m</option>
      <option value="0.393701">inch</option>
    </select>
  </body>
</html>

 

체크박스 활용 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        let [timer, timerId] = [0, 0]
        const h1 = document.querySelector('h1')
        const checkbox = document.querySelector('input')

        checkbox.addEventListener('change', (event) => {
          if (event.currentTarget.checked) {
            // 체크 상태
            timerId = setInterval(() => {
              timer += 1
              h1.textContent = `${timer}초`
            }, 1000)
          } else {
            // 체크 해제 상태
            clearInterval(timerId)
          }
        })
      })
    </script>
  </head>
  <body>
    <input type="checkbox">
    <span>타이머 활성화</span>
    <h1></h1>   
  </body>
</html>

 

라디오 버튼 활용 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        // 문서 객체 추출하기
        const output = document.querySelector('#output')
        const radios = document.querySelectorAll('[name=pet]')

        // 모든 라디오 버튼에
        radios.forEach((radio) => {
          // 이벤트 연결
          radio.addEventListener('change', (event) => {
            const current = event.currentTarget
            if (current.checked) {
              output.textContent = `좋아하는 애완동물은 ${current.value}이시군요!`
            }
          })
        })       
      })
    </script>
  </head>
  <body>
    <h3># 좋아하는 애완동물을 선택해주세요</h3>
    <input type="radio" name="pet" value="강아지">
    <span>강아지</span>
    <input type="radio" name="pet" value="고양이">
    <span>고양이</span>
    <input type="radio" name="pet" value="햄스터">
    <span>햄스터</span>
    <input type="radio" name="pet" value="기타">
    <span>기타</span>
    <hr>
    <h3 id="output"></h3>
  </body>
</html>

 

기본 이벤트 막기

웹 브라우저는 마우스 오른쪽 버튼을 클릭하면 컨텍스트 메뉴를 출력한다.

이처럼 어떤 이벤트가 발생했을 때 웹 브라우저가 기본적으로 처리해주는 것을 기본 이벤트라고 부른다.

기본 이벤트를 제거할 때는 event 객체의 preventDefalut() 메소드를 사용한다.

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const imgs = document.querySelectorAll('img')
       
        imgs.forEach((img) => {
          img.addEventListener('contextmenu', (event) => {
            event.preventDefault()
          })
        })
      })
    </script>
  </head>
  <body>
    <img src="http://placekitten.com/300/300" alt="">
  </body>
</html>

위 코드를 실행하면 고양이 사진이 뜨는데 해당 사진위에서 마우스 우클릭을 하면 컨텍스트 메뉴가 뜨지 않는다.

 

체크할 때만 링크 활성화 예제

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        let status = false
        
        const checkbox = document.querySelector('input')
        checkbox.addEventListener('change', (event) => {
          status = event.currentTarget.checked
        })

        const link = document.querySelector('a')
        link.addEventListener('click', (event) => {
          if (!status) {
            event.preventDefault()
          }
        })
      })
    </script>
  </head>
  <body>
    <input type="checkbox">
    <span>링크 활성화</span>
    <br>
    <a href="http://hanbit.co.kr">한빛미디어</a>
  </body>
</html>

 

할일 목록 만들기

<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>
  <body>
    <h1>할 일 목록</h1>
    <input id="todo">
    <button id="add-button">추가하기</button>
    <div id="todo-list">


    </div>
  </body>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      // 문서 객체를 가져옵니다.
      const input = document.querySelector('#todo')
      const todoList = document.querySelector('#todo-list')
      const addButton = document.querySelector('#add-button')

      // 변수를 선언합니다.
      let keyCount = 0

      // 함수를 선언합니다.
      const addTodo = () => {
        // 입력 양식에 내용이 없으면 추가하지 않습니다.
        if (input.value.trim() === '') {
          alert('할 일을 입력해주세요.')
          return
        }

        // 문서 객체를 설정합니다.
        const item = document.createElement('div')
        const checkbox = document.createElement('input')
        const text = document.createElement('span')
        const button = document.createElement('button')

        // 문서 객체를 식별할 키를 생성합니다.
        const key = keyCount
        keyCount += 1

        // item 객체를 조작하고 추가합니다.
        item.setAttribute('data-key', key)
        item.appendChild(checkbox)
        item.appendChild(text)
        item.appendChild(button)
        todoList.appendChild(item)

        // checkbox 객체를 조작합니다.
        checkbox.type = 'checkbox'
        checkbox.addEventListener('change', (event) => {
          item.style.textDecoration
            = event.target.checked ? 'line-through' : ''
        })

        // text 객체를 조작합니다.
        text.textContent = input.value

        // button 객체를 조작합니다.
        button.textContent = '제거하기'
        button.addEventListener('click', () => {
          removeTodo(key)
        })

        // 입력 양식의 내용을 비웁니다.
        input.value = ''
      }

      const removeTodo = (key) => {
        // 식별 키로 문서 객체를 제거합니다.
        const item = document.querySelector(`[data-key="${key}"]`)
        todoList.removeChild(item)
      }

      // 이벤트 연결
      addButton.addEventListener('click', addTodo)
      input.addEventListener('keyup', (event) => {
        // 입력 양식에서 Enter 키를 누르면 바로 addTodo() 함수를 호출합니다.
        const ENTER = 13
        if (event.keyCode === ENTER) {
          addTodo()
        }
      })
    })
  </script>
</html>

 

localStorage 객체

localStorage 객체는 웹 브라우저의 클라이언트 측에 데이터를 저장하는 데 사용되는 웹 스토리지 객체 중 하나이다. localStorage는 키-값 쌍으로 데이터를 저장하고, 이 데이터는 도메인과 프로토콜에 국한되어 해당 도메인에서만 접근할 수 있다.

저장된 데이터는 사용자의 로컬 브라우저에 영구적으로 보존되며, 동일한 도메인의 모든 페이지에서 공유된다.

  • window 객체의 속성을 통해 localStorage에 접근할 수 있다.
window.localStorage

 

  • setItem(key, value): 지정된 키와 값을 사용하여 데이터를 저장한다. 만약 이미 같은 키가 존재하면 기존 값은 덮어쓴다.
localStorage.setItem('username', 'JohnDoe');

 

  • getItem(key): 지정된 키에 대한 값을 반환한다. 만약 해당 키가 없으면 null을 반환한다.
const username = localStorage.getItem('username');

 

  • removeItem(key): 지정된 키와 관련된 데이터를 삭제한다.
localStorage.removeItem('username');

 

  • clear(): 모든 데이터를 삭제하여 localStorage를 초기화한다.
localStorage.clear();

 

  • key(index): 지정된 인덱스에 해당하는 키를 반한한다. localStorage에 저장된 키의 순서는 추가된 순서를 따른다.
const firstKey = localStorage.key(0);

 

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const p = document.querySelector('p')
        const input = document.querySelector('input')
        const button = document.querySelector('button')

        const savedValue = localStorage.getItem('input')
        // localStorage.input도 가능합니다.
        if (savedValue) {
          input.value = savedValue
          p.textContent = `이전 실행 때의 마지막 값: ${savedValue}`
        }

        input.addEventListener('keyup', (event) => {
          const value = event.currentTarget.value
          localStorage.setItem('input', value)
          // localStorage.input = value도 가능합니다.
        })

        button.addEventListener('click', (event) => {
          localStorage.clear()
          input.value = ''
        })
      })
    </script>
  </head>
  <body>
    <p></p>
    <button>지우기</button>
    <input type="text">
  </body>
</html>

 

JSON 이용

Storage 타입은 문자열 데이터만 저장한다.

JSON.stringify()JSON.parse()는 각각 JavaScript 객체를 JSON 문자열로 변환하거나, JSON 문자열을 JavaScript 객체로 변환하는 메소드이다.

 

  • JSON.stringify() : 이 메소드는 JavaScript 객체를 JSON 문자열로 변환한다.
const myObject = { key1: 'value1', key2: 'value2' };
const jsonString = JSON.stringify(myObject);
console.log(jsonString); // '{"key1":"value1","key2":"value2"}'

 

  • JSON.parse() : 이 메소드는 JSON 문자열을 JavaScript 객체로 변환한다.
const jsonString = '{"key1":"value1","key2":"value2"}';
const myObject = JSON.parse(jsonString);
console.log(myObject); // { key1: 'value1', key2: 'value2' }

 

storage에서 객체 저장하고 꺼내기

let testObject = { 'one': 1, 'two': 2, 'three': 3 };

//JSON 객체 저장
localStorage.setItem('testObject', JSON.stringify(testObject));

//JSON 객체 불러오기
let retrievedObject = localStorage.getItem('testObject');

console.log('retrievedObject: ', JSON.parse(retrievedObject));

'JavaScript Category > JavaScript' 카테고리의 다른 글

[JavaScript] 클래스  (0) 2024.01.18
[JavaScript] 예외처리  (0) 2024.01.18
[JavaScript] 객체(Object)  (0) 2024.01.16
[JavaScript] 함수  (1) 2024.01.15
[JavaScript] 반복문  (0) 2024.01.14