자바 프레임 워크 9주차 / DI

2026. 4. 29. 12:552026 상반기 전공과목 공부/Java 프레임워크

반응형

Spring Boot 회원 관리 시스템 구현 및 계층별 아키텍처 분리

  1. 서비스 계층 인터페이스 및 구현체 분리 인터페이스 정의: MemberService를 생성하여 기능 규격을 정의하고 구현체 분리를 통한 유연한 아키텍처 기반 마련 빈 등록 설정: @Service 어노테이션을 구현체에 명시하여 스프링 컨테이너 관리 대상임을 알리고 서비스 로직 실행 환경 구축
  2. 의존성 주입(DI) 방식 고도화 및 Lombok 활용 생성자 주입 적용: @RequiredArgsConstructor를 사용해 final 필드에 대한 의존성을 자동으로 주입받도록 설정하여 코드 간결화 도메인 최적화: Member 클래스에 @Data와 @Builder 어노테이션을 적용하여 객체 생성의 가독성 및 데이터 접근성 향상
  3. 타임리프 연동 및 회원 등록 폼 구현 객체 바인딩: th:object와 th:field를 활용해 HTML 폼 데이터와 자바 객체(Member)를 실시간으로 연결 데이터 전달: @ModelAttribute를 사용하여 화면에서 입력한 정보를 컨트롤러에서 객체 형태로 안정적으로 수신
  4. 데이터베이스 스키마 정의 및 영속성 계층 구축 테이블 설계: MySQL 데이터베이스에 tbl_member 테이블을 생성하고 회원 관리를 위한 기초 데이터(admin) 입력 JDBC 연동: DataSource와 JdbcTemplate을 연결하여 실제 DB와 통신할 수 있는 리포지토리 환경 구축
  5. 리포지토리 구현 및 전체 계층 연동 데이터 저장: MemberRepository 인터페이스 및 구현체를 작성하고 SQL 실행을 통해 실제 데이터 저장 로직 완성 최종 흐름 연결: 컨트롤러-서비스-리포지토리 간의 호출 구조를 완성하고 실행 과정에서 발생한 테이블 명 불일치 등 오류 수정

[요약] Spring Boot 회원 관리 시스템 구현 및 계층별 아키텍처 구축

  1. 인터페이스 기반 서비스 설계로 비즈니스 로직의 확장성 및 유연성 확보
  2. 생성자 주입 및 Lombok 적용을 통한 코드 최적화 및 결합도 감소
  3. 타임리프 바인딩 기능을 활용한 사용자 입력 데이터 처리 구조 구축
  4. MySQL 및 JdbcTemplate 연동으로 영속성 계층의 데이터 저장 기능 구현
  5. 각 계층 간 유기적인 데이터 흐름 완성 및 시스템 안정화 처리 완료

 

[Spring Core] 제어의 역전(IoC)과 의존성 주입(DI) 총정리

1. 소프트웨어 설계의 배경과 목표

더보기
  • 강한 결합(Tightly Coupling): 관련 없는 기능 간 높은 의존성으로 인해 유지보수 및 수정이 난해함.
  • 느슨한 결합(Loosely Coupling): 요소 간 의존성을 줄여 유연성을 확보하는 설계 지향.
  • 학습 목표: Spring Core의 핵심인 IoC DI를 통해 객체 간 의존성을 관리하는 방법 습득.

    1. 강한 결합 vs 느슨한 결합 (The Battery Analogy)
    • 강한 결합 (Tightly Coupled): 일체형 배터리 스마트폰. 배터리가 고장 나면 폰 전체를 뜯거나 버려야 함.
      (수정이 어렵고 의존적임)
    • 느슨한 결합 (Loosely Coupled): 탈착형 배터리나 보조 배터리.
      배터리가 수명이 다하면 새 배터리로 그냥 갈아 끼우면 끝. (수정이 쉽고 독립적임)
    • 소프트웨어에서의 목표: 부품(클래스)을 언제든 갈아 끼울 수 있는 '탈착형' 구조를 만드는 것.

2. Spring Core의 정의 및 구성

더보기
  • 정의: Spring Framework의 필수 기술 집합이자 기반 기술.
  • 주요 구성:
    • IoC 컨테이너: 객체의 생명주기 관리 및 의존성 주입 담당.
    • POJO(Plain Old Java Object) 지원: 특정 프레임워크에 종속되지 않는 순수 자바 객체 활용.
    • AOP(관점 지향 프로그래밍): 핵심 로직과 공통 기능을 분리하여 모듈성 강화.
    • 기타: 이벤트 처리, 리소스 관리, i18n(국제화), 유효성 검사, SpEL 등.

3. 제어의 역전 (IoC, Inversion of Control)

더보기
  • 개념: 개발자가 가진 객체의 제어권(생성, 호출 등)을 프레임워크(컨테이너)로 넘기는 디자인 패턴.
  • 전통적 방식: 개발자 코드가 라이브러리를 직접 호출하고 관리함.
  • IoC 방식: 프레임워크가 개발자의 코드를 호출하여 흐름을 제어함.
  • 구현 방법: DI(의존성 주입), 서비스 로케이터 패턴, 전략 패턴 등.

    3. IoC (제어의 역전) : "요리사는 요리만 하세요"
    • 전통적 방식: 요리사가 시장에 가서 "A 농장 고기를 사오자"라고 직접 결정함. (개발자가 new로 직접 객체 생성)
    • IoC 방식: 요리사는 주방에 가만히 있고, 주방 매니저(Spring)가 재료를 딱딱 갖다 줌.
      요리사는 어떤 농장 고기인지 신경 안 쓰고 오직 '요리'라는 본연의 역할에만 집중함.
    • 결론: 객체의 생성과 관리를 개발자가 아닌 스프링 컨테이너가 대신 해주는 것.
  • 원래라면 요리사(개발자)가 재료(객체)를 직접 사고, 손질하고, 관리해야 했지만, IoC가 도입되면 상황이 바뀐다.

4. 의존성 주입 (DI, Dependency Injection)

더보기
  • 정의: 계층/클래스 간 필요한 의존 관계를 컨테이너(ApplicationContext)가 자동으로 연결하는 것.
  • 주요 주입 방법:
    1. 필드 주입 (Field Injection): 멤버 변수에 @Autowired를 사용하여 직접 주입. 코드 간결하나 외부 변경이 어려움.
    2. 생성자 주입 (Constructor Injection): 객체 생성 시점에 의존성 주입. 
      불변성 보장, 순환 참조 방지 등으로 가장 권장되는 방식.
    3. 세터 주입 (Setter Injection): setXxx() 메서드를 통해 주입. 선택적인 의존성 주입 시 활용.
  • Lombok 활용: @RequiredArgsConstructor 사용 시 final 필드에 대한 생성자 자동 생성 및 주입 가능.

    4. DI (의존성 주입) : "배달 왔습니다!"주입 방법 3가지 (비유)
    1. 생성자 주입 (가장 권장): "식당 오픈할 때 아예 냉장고를 들여놓고 시작함."
      처음에 한 번 딱 세팅되면 바꿀 수 없어 안전함. (불변성 보장)
    2. 세터(Setter) 주입: "영업 중간에 재료가 떨어지면 그때그때 보충함."
      나중에 바꿀 수 있는 유연함이 있지만, 재료가 안 왔는데 요리를 시작할 위험이 있음.
    3. 필드 주입: "요리사 주머니에 재료를 몰래 찔러 넣어줌." 코드는 짧지만,
      외부에서 재료를 확인하거나 바꾸기 힘들어 권장하지 않음.
  • IoC를 실제로 구현하는 방법이 바로 DI입니다. 필요한 객체를 외부에서 '주입'해 주는 것이죠.

5. @Autowired의 원리 및 특징

더보기
  • 역할: 스프링 컨테이너가 관리하는 빈(Bean) 중 적절한 객체를 찾아 자동으로 주입.
  • 매칭 순서: 기본적으로 타입(Type) 매칭 수행 → 중복 시 이름(Name)으로 매칭.
  • Spring 4.3 이후 변화: 클래스 내 생성자가 하나뿐이라면 @Autowired 생략 가능.

6. Spring Bean과 POJO

더보기
  • Spring Bean: Spring 컨테이너(ApplicationContext)가 생성하고 관리하는 인스턴스.
    • 일반적으로 서비스, 레파지토리 레이어 클래스들이 대상임.
  • Java Bean: new 연산자로 개발자가 직접 생성하는 일반적인 인스턴스 (예: DTO, Entity).
  • 등록 방법:
    • 어노테이션: @Component, @Service, @Repository, @Controller.
    • 설정 클래스: @Configuration 내 @Bean 정의.
    • XML: <bean id="..." class="..." /> 파일 설정.

      6. Spring Bean : "관리 대상 리스트"
      • 스프링이라는 주방 매니저의 장부에 등록된 '공식 재료/도구'들.
      • @Component, @Service 같은 어노테이션을 붙이면 "이건 매니저님이 관리해 주세요!"라고 등록하는 것
    • 스프링이 관리하는 객체를 특별히 '빈(Bean)'이라고 부릅니다.

7. 자동 와이어링 (Auto-wiring) 방식 (XML 기준)

더보기
  • byType: 필드 타입과 일치하는 빈을 찾아 주입 (기본 방식).
  • byName: 필드명/세터명과 빈의 ID가 일치하는 경우 주입.
  • constructor: 생성자의 매개변수 타입과 일치하는 빈 주입.
  • autodetect: 생성자 방식을 먼저 시도하고 실패 시 타입 방식 수행.
  • 모호성 해결:
    • primary="true": 여러 후보 중 최우선 빈 선정.
    • autowire-candidate="false": 해당 빈을 자동 주입 대상에서 제외.

8. 의존성 주입(DI)의 기대효과

더보기
  • 유지보수성 향상: 코드 수정 없이 설정 변경만으로 구현체(JDBC ↔ JPA 등) 교체 가능.
  • 테스트 용이성: 가짜 객체(Mock) 주입을 통한 단위 테스트가 수월함.
  • 객체 지향적 설계: 인터페이스 기반 설계를 통해 결합도는 낮추고 응집도는 높임.

정리 : Spring Core는 IoC와 DI를 통해 객체 간의 복잡한 연결을 자동화하여

개발자가 비즈니스 로직에만 집중할 수 있게 돕는 핵심 프레임워크임.


MemberService interface, MemberServiceImpl를 만듬

MemberService

더보기
package indusw.sba.admin2024001910a.service;

import indusw.sba.admin2024001910a.domain.Member;

public interface MemberService {
   int create(Member m);
   Member readOne(Member m);
}

MemberServiceImpl

더보기
package indusw.sba.admin2024001910a.service;

import indusw.sba.admin2024001910a.domain.Member;
import org.springframework.stereotype.Service;

@Service // Service 형 Spring Bean 임을 컴파일러/컨테이너에 알림

public class MemberServiceImpl  implements  MemberService{
    @Override
    public int create(Member m) {
        System.out.println("MemberService - Create");
        return 0;
    }

    @Override
    public Member readOne(Member m) {
        return null;
    }
}



 


MemberController 코드 수정

더보기
package indusw.sba.admin2024001910a.controller;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.service.MemberService;
import indusw.sba.admin2024001910a.service.MemberServiceImpl;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
public class MemberController {
    MemberService memberService = new MemberServiceImpl();

    @GetMapping("/create")
    public String create(Model model) {
        Member member = new Member();
        memberService.create(member);
        return "/main/index";
    }
}

실행해서 http://localhost:9000/members/create로 접속하면

 

로그에 MemberService - Create가 나오게 된다


MemberServiceImpl2를 만들고

더보기
package indusw.sba.admin2024001910a.service;

import indusw.sba.admin2024001910a.domain.Member;

public class MemberServiceImpl2 implements MemberService{
    @Override
    public int create(Member m) {
        return 0;
    }

    @Override
    public Member readOne(Member m) {
        return null;
    }
}

MemberController

 

더보기
package indusw.sba.admin2024001910a.controller;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.service.MemberService;
import indusw.sba.admin2024001910a.service.MemberServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;
    @Autowired // Spring 4.3이후 생성자가 하나인 경우 생략 가능
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/create")
    public String create(Model model) {
        Member member = new Member();
        memberService.create(member);
        return "/main/index";
    }
}

spring boot에서는 아래 코드로도 가능하다

@Autowired 생략, @RequiredArgsConstructor사용해서 생성자(의존성) 주입 (Constructor Injection)을 생략 가능하다

package indusw.sba.admin2024001910a.controller;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.service.MemberService;
//import indusw.sba.admin2024001910a.service.MemberServiceImpl;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.beans.factory.annotation.Qualifier;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    //@Autowired // Spring 4.3이후 생성자가 하나인 경우 생략 가능 , DI(Dependency Injection) - Constructor(생성자) 활용
    /*
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
     */

    @GetMapping("/reg-form")
    public String getRegForm(Model model) {
        return "/members/register";
    }

    @GetMapping("/create")
    public String create(Model model) {
        Member member = new Member();
        memberService.create(member);
        return "/main/index";
    }
}

MemberRepository 생성

더보기
package indusw.sba.admin2024001910a.repository;

public interface MemberRepository {
}

MemberRepositoryImpl 생성

더보기
package indusw.sba.admin2024001910a.repository;

import org.springframework.stereotype.Repository;

@Repository

public class MemberRepositoryImpl implements  MemberRepository{
}

 

MemberController 수정

더보기
package indusw.sba.admin2024001910a.controller;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.repository.MemberRepository;
import indusw.sba.admin2024001910a.service.MemberService;
//import indusw.sba.admin2024001910a.service.MemberServiceImpl;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.beans.factory.annotation.Qualifier;
//import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
//@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
//    private final MemberRepository memberRepository;
    //@Autowired // Spring 4.3 이후 생략 가능, DI(Dependency Injection) - Constructor(생성자) 활용

    public MemberController(MemberService memberService, MemberRepository memberRepository) {
        this.memberService = memberService;
//        this.memberRepository = memberRepository;
    }


    @GetMapping("/reg-form")
    public String getRegForm(Model model) {
        return "/members/register";
    }

    @PostMapping("/register")
    public String postRegister(Model model) {
        Member member = new Member();
        memberService.create(member);
        return "/main/index";
    }
}

MemberRepositoryImpl 수정

더보기
package indusw.sba.admin2024001910a.service;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.repository.MemberRepository;
import org.springframework.stereotype.Service;

@Service  // Service 형 Spring Bean 임을 컴파일러/컨테이너에 알림
public class MemberServiceImpl implements MemberService {
    private MemberRepository memberRepository;

    public MemberRepository getMemberRepository(MemberRepository memberRepository) {
        return memberRepository;
    }

    @Override
    public int create(Member m) {
        System.out.println("MemberService - Create");
        return 0;
    }

    @Override
    public Member readOne(Member m) {
        return null;
    }
}

sb.html

../members/register.html을 ../members/reg-form로 변경

더보기
<a class="collapse-item" href="../members/reg-form">Register</a>

 

 

실행하면

register를 누르면 

http://localhost:9000/members/reg-form 로 링크가 변경된 모습


register.html

상단 부분을 변경

더보기
<html lang="en" xmlns:th="http://www.thymeleaf.org">

Member.java 코드 수정

더보기
package indusw.sba.admin2024001910a.domain;

import lombok.*;
import org.springframework.context.annotation.Bean;

//@Getter
//@Setter
//@NoArgsConstructor
//@AllArgsConstructor
//@EqualsAndHashCode
//@ToString
@Data
@Builder

public class Member {
    private long id;
    private String name;
    private String email;
    private String pw;

}

 


 

MemberController

더보기
package indusw.sba.admin2024001910a.controller;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.repository.MemberRepository;
import indusw.sba.admin2024001910a.service.MemberService;
//import indusw.sba.admin2024001910a.service.MemberServiceImpl;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.beans.factory.annotation.Qualifier;
//import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
//@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
//    private final MemberRepository memberRepository;
    //@Autowired // Spring 4.3 이후 생략 가능, DI(Dependency Injection) - Constructor(생성자) 활용

    public MemberController(MemberService memberService, MemberRepository memberRepository) {
        this.memberService = memberService;
//        this.memberRepository = memberRepository;
    }


    @GetMapping("/reg-form")
    public String getRegForm(Model model) {
        model.addAttribute("dto", Member.builder().build());
        return "/members/register";
    }

    @PostMapping("/register")
    public String postRegister(Model model) {
        Member member = Member.builder().build();
        // new 와 동일한 기능이지만, 필드를 명시적으로 지정하므로 모호성을 줄임 
        Member member = new Member();
        memberService.create(member);
        return "/main/index";
    }
}

register.html 39줄 부터 수정

더보기
<form th:action="@{'/members/register'}" th:method="POST"  th:object="${dto}"
      action="#"  class="user">
    <div class="form-group row">
        <div class="col-sm-6 mb-3 mb-sm-0">
            <input type="text" name="id" class="form-control form-control-user" id="exampleFirstName"
                   placeholder="ID" th:field="*{id}">
        </div>
        <div class="col-sm-6">
            <input type="text" name="name" class="form-control form-control-user" id="exampleLastName"
                   placeholder="Name" th:field="*{name}">
        </div>
        <div class="form-group row">
            <div class="col-sm-6 mb-3 mb-sm-0">
                <input type="password" name="pw" class="form-control form-control-user"
                       id="exampleInputPassword" placeholder="Password" th:field="*{pw}">
            </div>
            <div class="col-sm-6">
                <input type="password" name="email" class="form-control form-control-user"
                       id="exampleRepeatPassword" placeholder="RepeatPassword">
            </div>
        </div>
    </div>
    <div class="form-group">
        <input type="email" name="email" class="form-control form-control-user" id="exampleInputEmail"
               placeholder="Email" th:field="*{email}">
    </div>

    <input type="submit" href="login.html" class="btn btn-primary btn-user btn-block">
        Register Account
    </input>

MemberController

postRegister부분 코드 수정

더보기
@PostMapping("/register")
public String postRegister(@ModelAttribute("dto") Member dto, Model model) {
    System.out.println(dto.getEmail());
    // new와 동일한 기능이지만, 필드를 명시적으로 지정함으로써 모호성을 줄임
    memberService.create(dto);
    model.addAttribute("dto", dto); // view에서 model에 포함된 dto 속성을 사용할 수 있도록 추가
    return "/main/index";
}

실행하면

입력한 email이 로그에 찍히고 register에서 main/index로 잘 넘어감


register에서 sumit버튼을 변경

<input type="submit" value="Register Account" class="btn btn-primary btn-user btn-block"/>

MemberServiceImpl2를 삭제

 

MemberController에서

create에 Implementation(s)를 실행한다

더보기
package indusw.sba.admin2024001910a.service;

import indusw.sba.admin2024001910a.domain.Member;
import indusw.sba.admin2024001910a.repository.MemberRepository;
import org.springframework.stereotype.Service;

@Service  // Service 형 Spring Bean 임을 컴파일러/컨테이너에 알림
public class MemberServiceImpl implements MemberService {
    private MemberRepository memberRepository;

    public MemberRepository getMemberRepository(MemberRepository memberRepository) {
        return memberRepository;
    }

    @Override
    public int create(Member m) {
        System.out.println("MemberService - Create");
        return 0;
    }

    @Override
    public Member readOne(Member m) {
        return null;
    }
}

ab_2024001910(본인학번) 데이터베이스를 만들고 use를 하고 tbl_member 테이블을 만들고 insert를 해준다

더보기
create table tbl_member (
                            id int(11) not null auto_increment primary key, -- 회원 고유 번호
                            name varchar(30) not null,                      -- 이름
                            pw varchar(30) not null,                        -- 비밀번호
                            email varchar(30) not null unique,              -- 이메일 (중복 불가)
                            phone varchar(30),                              -- 전화번호
                            address varchar(100)                            -- 주소
);

insert into tbl_member(name, pw, email)
values('admin', 'cometrue', 'admin@induk.ac.kr');
select  * from tbl_member;


MemberRepositoryImpl 코드 수정

더보기
package indusw.sba.admin2024001910a.repository;

import indusw.sba.admin2024001910a.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;

@Repository
public class MemberRepositoryImpl implements MemberRepository {
    private JdbcTemplate jdbcTemplate;

    public MemberRepositoryImpl(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public int insert(Member domain) {
        String sql = "insert into member(name, pw, email) values(?, ?, ?)";
        try {
            jdbcTemplate.update(sql, domain.getName(), domain.getPw(), domain.getEmail());
            return 1;
        } catch (Exception e) {
            System.out.println(e);
            return 0;
        }
    }
}

MemberRepository

더보기
package indusw.sba.admin2024001910a.repository;

import indusw.sba.admin2024001910a.domain.Member;

public interface MemberRepository {
    int insert(Member domain);
}

MemberServiceImpl

더보기
@Service    // Service 형 Spring Bean 임을 컴파일러/컨테이너에 알림
public class MemberServiceImpl implements MemberService {
    private MemberRepository memberRepository;
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public int create(Member m) {
        int ret = memberRepository.insert(m);
        return ret;
    }

    @Override
    public Member readOne(Member m) { return null; }
}

 

실행하면 오류뜸 수정