JAVA Spring 공식문서 코드 최적화 해보기

박지성 학부생·2024년 2월 28일
2

BackEnd Develop log

목록 보기
27/27

코드 리팩토링 with Spring Boot CRUD Tutorial with Spring MVC, Spring Data JPA, Thymeleaf, Hibernate, MySQL

자바 가이드 문서예시를 스프링부트 최신 버전에 맞추어 코드 리팩토링 및 성능 개선함
자바 가이드 원본보기 클릭
리팩토링 깃허브 레포 클릭

Selected below dependencies while creating spring boot project using spring initializr:

  • Spring Web
  • Thymeleaf
  • Spring Data JPA
  • MySQL Driver
  • Spring Boot Devtools

파일 구조

  • 📦main
    • 📂java
      • 📂com
        • 📂walab
          • 📂SpringBootCRUDTutorial
            • 📂controller
              • 📄StudentController.java
            • 📂entity
              • 📄Student.java
            • 📂repository
              • 📄StudentRepository.java
            • 📂service
              • 📄StudentService.java
            • 📄SpringBootCrudTutorialApplication.java
    • 📂resources
      • 📂static
      • 📂templates
        • 📄create_student.html
        • 📄edit_student.html
        • 📄students.html
      • 📄application.yml

Entity 개선

공식문서 기존코드

@Entity
@Table(name = "students")
public class Student {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(name = "first_name", nullable = false)
	private String firstName;
	
	@Column(name = "last_name")
	private String lastName;
	
	@Column(name = "email")
	private String email;
	
	public Student() {
		
	}
	
	public Student(String firstName, String lastName, String email) {
		super();
		this.firstName = firstName;
		this.lastName = lastName;
		this.email = email;
	}
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
}

리팩토링 코드

@Entity
@Table(name = "students")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;
}

리팩토링된 코드에는 @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor 어노테이션을 사용

  • 코드 간결성이 눈에 띄게 향상되어, 소스 코드 읽기가 훨씬 쉬워짐.
  • getter와 setter 메소드를 포함한 생성자들이 자동으로 생성되므로, 유지보수가 훨씬 용이함.
  • 필드 변경 시 자동으로 관련 메소드가 업데이트되기 때문에, 코드 수정이 더 빠르고 간단함.
  • Lombok 사용으로 인한 컴파일 시간의 미세한 증가는 있지만, 실행 시간 성능에는 영향 없음.
  • 개발 효율성과 코드 관리성이 크게 개선됨.

Repository 개선

공식문서 기존코드

public interface StudentRepository extends JpaRepository<Student, Long>{

}

리팩토링 코드

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

}

@Repository 어노테이션의 유무: 두 번째 선언에는 @Repository 어노테이션이 명시적으로 포함되어 있음.
이 어노테이션은 스프링 컨테이너에게 해당 인터페이스가 데이터 액세스 계층의 컴포넌트임을 알려주는 역할을 함.

  • 스프링의 예외 변환: @Repository 어노테이션은 스프링의 데이터 접근 예외를 스프링의 DataAccessException으로 자동 변환하는 추가적인 이점을 제공함. 이는 데이터베이스 작업 중 발생할 수 있는 예외 처리를 일관성 있게 관리할 수 있게 해줌.
  • 컴포넌트 스캐닝: @Repository 어노테이션을 사용하면 스프링이 자동으로 컴포넌트 스캐닝을 통해 해당 인터페이스의 구현체를 빈으로 등록함. 이는 명시적으로 빈을 등록하는 과정을 생략할 수 있게 해줌.
  • @Repository 어노테이션을 포함 => 데이터 액세스 계층의 컴포넌트를 명확히 식별하고, 예외 처리 및 빈의 자동 등록과 같은 장점이 있음.

Service 개선

공식문서 기존코드

public interface StudentService {
	List<Student> getAllStudents();
	
	Student saveStudent(Student student);
	
	Student getStudentById(Long id);
	
	Student updateStudent(Student student);
	
	void deleteStudentById(Long id);
}

}
@Service
public class StudentServiceImpl implements StudentService{

	private StudentRepository studentRepository;
	
	public StudentServiceImpl(StudentRepository studentRepository) {
		super();
		this.studentRepository = studentRepository;
	}

	@Override
	public List<Student> getAllStudents() {
		return studentRepository.findAll();
	}

	@Override
	public Student saveStudent(Student student) {
		return studentRepository.save(student);
	}

	@Override
	public Student getStudentById(Long id) {
		return studentRepository.findById(id).get();
	}

	@Override
	public Student updateStudent(Student student) {
		return studentRepository.save(student);
	}

	@Override
	public void deleteStudentById(Long id) {
		studentRepository.deleteById(id);	
	}
}

리팩토링 코드

@RequiredArgsConstructor
@Service
public class StudentService {
    private final StudentRepository studentRepository;

    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

    public Student getStudentById(Long id) {
        return studentRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Student not found for id " + id));
    }

    public Student saveStudent(Student student) {
        return studentRepository.save(student);
    }

    public Student updateStudent(Long id, Student studentDetails) {
        return studentRepository.findById(id)
                .map(existingStudent -> {
                    existingStudent.setFirstName(studentDetails.getFirstName());
                    existingStudent.setLastName(studentDetails.getLastName());
                    existingStudent.setEmail(studentDetails.getEmail());
                    return studentRepository.save(existingStudent);
                })
                .orElseThrow(() -> new RuntimeException("Student not found for id " + id));
    }



    public void deleteStudentById(Long id) {
        studentRepository.findById(id).orElseThrow(() -> new RuntimeException("Student not found for id " + id));
        studentRepository.deleteById(id);
    }

}
  • 생성자 주입 자동화: @RequiredArgsConstructor를 통해 코드 간결화 및 의존성 주입 간소화를 달성, 불변성 보장 및 순환 참조 방지!!!(중요)
  • 예외 처리 향상: .orElseThrow() 사용으로, 학생 조회 및 삭제 시 명확한 예외 피드백 제공함.
  • 업데이트 로직 최적화: 학생 정보 업데이트 시 변경 감지를 활용, 데이터베이스와의 불필요한 상호작용 감소시킴.
  • 코드 및 프로젝트 간결성: Lombok 어노테이션과 서비스 구현 병합으로 코드 단순화 및 프로젝트 구조 단순화를 이룸, 가독성 및 유지보수성 향상 및 개발 속도 증가에 기여함.
  • 성능 영향 최소화: 런타임 성능에 직접적 영향은 없지만, 개발 및 유지보수의 효율성 증가

Controller 개선

공식문서 기존코드

@Controller
public class StudentController {
	
	private StudentService studentService;

	public StudentController(StudentService studentService) {
		super();
		this.studentService = studentService;
	}
	
	// handler method to handle list students and return mode and view
	@GetMapping("/students")
	public String listStudents(Model model) {
		model.addAttribute("students", studentService.getAllStudents());
		return "students";
	}
	
	@GetMapping("/students/new")
	public String createStudentForm(Model model) {
		
		// create student object to hold student form data
		Student student = new Student();
		model.addAttribute("student", student);
		return "create_student";
		
	}
	
	@PostMapping("/students")
	public String saveStudent(@ModelAttribute("student") Student student) {
		studentService.saveStudent(student);
		return "redirect:/students";
	}
	
	@GetMapping("/students/edit/{id}")
	public String editStudentForm(@PathVariable Long id, Model model) {
		model.addAttribute("student", studentService.getStudentById(id));
		return "edit_student";
	}

	@PostMapping("/students/{id}")
	public String updateStudent(@PathVariable Long id,
			@ModelAttribute("student") Student student,
			Model model) {
		
		// get student from database by id
		Student existingStudent = studentService.getStudentById(id);
		existingStudent.setId(id);
		existingStudent.setFirstName(student.getFirstName());
		existingStudent.setLastName(student.getLastName());
		existingStudent.setEmail(student.getEmail());
		
		// save updated student object
		studentService.updateStudent(existingStudent);
		return "redirect:/students";		
	}
	
	// handler method to handle delete student request
	
	@GetMapping("/students/{id}")
	public String deleteStudent(@PathVariable Long id) {
		studentService.deleteStudentById(id);
		return "redirect:/students";
	}	
}

리팩토링 코드

@RequiredArgsConstructor
@Controller
public class StudentController {
    private final StudentService studentService;

    @GetMapping("/students")
    public String listStudents(Model model) {
        model.addAttribute("students", studentService.getAllStudents());
        return "students";
    }

    @GetMapping("/students/new")
    public String createStudentForm(Model model) {
        model.addAttribute("student", new Student());
        return "create_student";
    }

    @GetMapping("/students/edit/{id}")
    public String editStudentForm(@PathVariable Long id, Model model) {
        model.addAttribute("student", studentService.getStudentById(id));
        return "edit_student";
    }

    @PostMapping("/students")
    public String saveStudent(@ModelAttribute("student") Student student) {
        studentService.saveStudent(student);
        return "redirect:/students";
    }

    @PostMapping("/students/{id}")
    public String updateStudent(@PathVariable Long id, @ModelAttribute("student") Student student, Model model) {
        studentService.updateStudent(id, student);
        return "redirect:/students";
    }

    @GetMapping("/students/{id}")
    public String deleteStudent(@PathVariable Long id) {
        studentService.deleteStudentById(id);
        return "redirect:/students";
    }

}
  • 생성자 주입 자동화:
    @RequiredArgsConstructor 어노테이션을 사용하여 생성자 주입이 자동화 => StudentService에 대한 의존성 주입이 간소화되고, 생성자 코드를 명시적으로 작성할 필요가 없어짐
  • 코드의 간결성 향상:
    리팩토링 과정에서 중복 코드가 제거되어, 전체적인 코드의 양이 줄고 가독성이 향상. updateStudent 메서드에서 existingStudent 객체를 별도로 생성하고 설정하는 과정이 StudentService 내의 로직으로 이동
  • 일관된 데이터 처리(중요):
    StudentService 내에서 데이터 처리 로직을 중앙집중화함 => 컨트롤러는 더욱 단순화되고 서비스 레이어가 비즈니스 로직을 처리하는 책임을 갖게 됨 => 이는 아키텍처의 분리 원칙을 더욱 잘 따르는 구조
  • 향상된 예외 처리:
    getStudentById와 deleteStudentById 메소드에서 .orElseThrow()를 사용하여, 존재하지 않는 학생 ID에 대한 요청 시 명확한 예외 처리를 수행 => 이는 잠재적 오류를 식별 용이
  • 유지보수와 확장성:
    코드가 더 간결하고 명확해짐 => 유지보수, 기능 확장 시 유리. @RequiredArgsConstructor 사용으로 인한 의존성 주입의 자동화는 새로운 의존성이 추가되거나 변경될 때 개발자가 수행해야 하는 작업을 줄여줌

결과 화면(기존 공식문서와 결과는 같음)






직접적인 성능향상이 발생할 정도의 서비스가 아니지만 코드의 간결함과 예외처리, mvc패턴에서 각 계층의 역할에만 집중할 수 있게 리팩토링을 해보았습니다.

profile
참 되게 살자

0개의 댓글