회원 가입하고 그 결과를 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를 가져와 사용할 수 있다.
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록 해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
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으로 등록한다.
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");
}
}
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
비즈니스 요구사항 정리
데이터 : 회원 ID, 이름
기능 : 회원 등록(단, 중복 이름 허용X), 회원 조회
아직 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;
}
}
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() 예상 값과 실제 값을 비교하여 두 값이 동일한지를 확인하는 데 사용됩니다. 이 메서드는 테스트 케이스의 성공 여부를 판단하는 데 주로 활용
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]; // 배열의 범위를 벗어난 인덱스에 접근
});
}
}
이후 localhost:8080/hello-static.html 로 들어가면 해당 내용이 그대로 올라와있는 것을 알 수 있다.
이를 통해 알 수 있는 사실은 원하는 파일을 resources/static 폴더에 넣으면 그대로 반환이 된다는 것이다.
대신 이 방법은 정적 컨텐츠 방식이기 때문에 이후 가공이 들어가지 않고 그대로 반환이 된다.
웹 브라우저에서 localhost:8080/hello-static.html에 대해서 내장 서버인 tomcat에게 요청한다.
tomcat 서버는 hello-static.html에 대해서 다시 Spring에게 요청한다.
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을 찾아갈 것이다.
세번째 줄을 보면 ${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이 처리된다.
spring boot를 띄울 때 같이 띄우는 내장 tomcat 서버를 먼저 거친다. tomcat 서버는 hello-mvc 요청이 왔음을 spring 컨테이너로 넘긴다.
spring 컨테이너는 helloController에 hello-mvc가 @GetMapping을 통해 hello-mvc가 mapping 되어 있는 것을 확인하고, mapping 되어있는 해당 메서드를 실행한다. 해당 메서드는 "hello-template"라는 String을 반환하고, model에는 (key : name, value : spring)이라는 값을 넣는다. 그리고 이 값들을 viewResolver에게 넘겨준다.
viewResolver가 동작한다. viewResolver는 view를 찾아주고, 템플릿 엔진을 연결시켜주는 역할을 한다. viewResolver가 templates/hello-template.html 을 찾아서 Thymeleaf 템플릿 엔진에게 처리 요청을 한다.
템플릿 엔진인 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 형태로 응답으로 반환다.
Spring은 먼저 Controller에 hello-api가 맵 되어 있는지 확인한다. 이 때, 만일 @ResponseBody가 적혀있지 않았다면, viewResolver에게 전달해서 template engine이 html로 변환한다.
메서드에 @ResponseBody 어노테이션이 있고, String을 반환하는 것이 아닌 hello 객체를 반환한다. 따라서 기본적으로 JSON 방식으로 데이터를 만들어서 http응답에 반환한다.
helloController에서 메서드가 실행되어 hello 객체를 넘기게 되면 "HttpMessageConverter"가 작동한다. 만일 단순 문자열을 반환했다면 "StringConverter"가 동작하고, 객체를 반환했다면 "JsonConverter"가 동작한다. 이 상황에서는 hello 객체를 받았기 때문에 JsonConverter가 동작하여 hello 객체의 내용을 JSON 형식으로 바꾼다.
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을 사용하기 위해서는 위 항목들이 필요하다는 뜻이다.
이런 식으로 해당 기능을 사용하기 위해 다른 어떤 기능들이 필요한지 계층 구조로 보여준다.
중요한 항목은 아래와 같다.
spring-boot-starter-web을 열어보면 하위에 tomcat이 있다. tomcat은 웹 서버에 내장되어 있다. 이전에는 웹 서버에 tomcat을 직접 설치했어야 했지만 현재는 따로 설정을 하지 않아도 된다.
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을 만들어주자.
자바스크립트는 유연한 언어라서 엄청나게 큰 잘못을 하지 않는이상 예외를 발생시키지 않기 때문에, 프로그램의 안정성을 위해서 예외를 강제로 발생시켜야 하는 경우가 종종 발생한다.
// 단순하게 예외를 발생
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);
}
이 글은 혼자공부하는 자바스크립트(저자 : 윤인성)의 책 내용과 유튜브 동영상을 참고하여 개인적으로 정리하는 글임을 알립니다.
문서 객체 조작
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>
글자 조작
innerHTML과 textContent는 JavaScript에서 DOM 요소의 내용을 조작하는 데 사용되는 두 가지 속성이다.
보통 텍스트만 변경해야 하는 경우에는 textContent를 사용하고, HTML을 조작해야 하는 경우에는 innerHTML을 사용한다.
그러나 innerHTML을 사용할 때는 새 HTML을 파싱하고 해당 요소의 내용을 완전히 대체하기 때문에, 기존에 연결된 이벤트 핸들러 등이 손실될 수 있다.
// 요소의 HTML 내용을 변경
element.innerHTML = '<p>새로운 HTML</p>';
// 요소의 텍스트 내용을 변경
element.textContent = '새로운 텍스트';
이벤트 리스너와 이벤트 핸들러는 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 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>)에서 내용을 변경하고 포커스를 잃었을 때 발생하는 이벤트이다. 이 이벤트는 사용자가 값을 변경하고 입력을 완료하거나 해당 요소를 떠날 때 한 번만 발생한다.