자바를 처음 배웠을 때는 그냥 “Hello World” 출력하는 게 전부였다. 하지만 스프링 부트를 만나고 나서야 자바의 진짜 매력을 알게 됐다. 이제는 웹 애플리케이션을 몇 시간 만에 뚝딱 만들 수 있다.
자바를 제대로 쓰려면 기본기를 탄탄히 해야 한다. 특히 객체지향 프로그래밍과 컬렉션 프레임워크는 필수다.
자바의 핵심은 클래스다. 현실 세계의 개념을 코드로 표현할 수 있다.
public class User {
private String name;
private String email;
private int age;
// 생성자
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getter와 Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 비즈니스 로직
public boolean isAdult() {
return age >= 18;
}
@Override
public String toString() {
return "User{name='" + name + "', email='" + email + "', age=" + age + "}";
}
}
데이터를 효율적으로 관리하려면 컬렉션을 잘 써야 한다.
import java.util.*;
public class CollectionExample {
public static void main(String[] args) {
// List - 순서가 있는 컬렉션
List<String> names = new ArrayList<>();
names.add("김철수");
names.add("이영희");
names.add("박민수");
// Set - 중복을 허용하지 않는 컬렉션
Set<Integer> numbers = new HashSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(2); // 중복이므로 추가되지 않음
// Map - 키-값 쌍을 저장하는 컬렉션
Map<String, Integer> scores = new HashMap<>();
scores.put("김철수", 95);
scores.put("이영희", 87);
scores.put("박민수", 92);
// 람다 표현식과 스트림 API
names.stream()
.filter(name -> name.startsWith("김"))
.forEach(System.out::println);
}
}
자바에서 예외 처리는 필수다. 프로그램의 안정성을 위해 반드시 해야 한다.
public class ExceptionExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("결과: " + result);
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다: " + e.getMessage());
} catch (Exception e) {
System.out.println("예상치 못한 오류: " + e.getMessage());
} finally {
System.out.println("예외 처리 완료");
}
}
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("0으로 나눌 수 없습니다");
}
return a / b;
}
}
스프링 부트는 자바 웹 개발을 쉽게 만들어준다. 설정이 복잡했던 스프링을 간단하게 사용할 수 있다.
Spring Initializr를 사용해서 프로젝트를 생성하자.
의존성 선택:
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── demo/
│ │ ├── DemoApplication.java
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ └── entity/
│ └── resources/
│ ├── application.properties
│ └── static/
└── test/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
REST API는 현대 웹 개발의 핵심이다. 스프링 부트로 쉽게 만들 수 있다.
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
@Autowired
private UserService userService;
// 모든 사용자 조회
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(users);
}
// 특정 사용자 조회
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
// 새 사용자 생성
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
// 사용자 정보 수정
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
if (updatedUser != null) {
return ResponseEntity.ok(updatedUser);
} else {
return ResponseEntity.notFound().build();
}
}
// 사용자 삭제
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
boolean deleted = userService.deleteUser(id);
if (deleted) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
}
}
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
public User createUser(User user) {
// 이메일 중복 체크
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("이미 존재하는 이메일입니다");
}
return userRepository.save(user);
}
public User updateUser(Long id, User userDetails) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
user.setAge(userDetails.getAge());
return userRepository.save(user);
}
public boolean deleteUser(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
}
JPA를 사용해서 데이터베이스를 쉽게 다룰 수 있다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(nullable = false)
private int age;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 기본 생성자
public User() {}
// 생성자
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getter와 Setter
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 이메일로 사용자 찾기
Optional<User> findByEmail(String email);
// 이름으로 사용자 찾기
List<User> findByName(String name);
// 나이 범위로 사용자 찾기
List<User> findByAgeBetween(int minAge, int maxAge);
// 이름으로 검색 (부분 일치)
List<User> findByNameContaining(String name);
// 커스텀 쿼리
@Query("SELECT u FROM User u WHERE u.age > :age ORDER BY u.createdAt DESC")
List<User> findUsersOlderThan(@Param("age") int age);
// 네이티브 쿼리
@Query(value = "SELECT * FROM users WHERE age > ?1", nativeQuery = true)
List<User> findUsersOlderThanNative(int age);
}
# application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# H2 콘솔 활성화
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
전역 예외 처리를 통해 일관된 에러 응답을 제공하자.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e) {
ErrorResponse error = new ErrorResponse("RUNTIME_ERROR", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", message);
return ResponseEntity.badRequest().body(error);
}
}
// 에러 응답 클래스
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
this.timestamp = LocalDateTime.now();
}
// Getter와 Setter
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}
애플리케이션의 상태를 모니터링하기 위해 로깅을 활용하자.
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/api/users")
public ResponseEntity<List<User>> getAllUsers() {
log.info("모든 사용자 조회 요청");
try {
List<User> users = userService.getAllUsers();
log.info("사용자 조회 성공: {}명", users.size());
return ResponseEntity.ok(users);
} catch (Exception e) {
log.error("사용자 조회 실패", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
log.info("새 사용자 생성 요청: {}", user.getEmail());
try {
User createdUser = userService.createUser(user);
log.info("사용자 생성 성공: ID={}", createdUser.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
} catch (Exception e) {
log.error("사용자 생성 실패: {}", e.getMessage());
return ResponseEntity.badRequest().build();
}
}
}
성능 향상을 위해 캐싱을 활용하자.
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
log.info("데이터베이스에서 사용자 조회: ID={}", id);
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(Long id, User userDetails) {
log.info("사용자 정보 수정: ID={}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
user.setAge(userDetails.getAge());
return userRepository.save(user);
}
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
log.info("사용자 캐시 초기화");
}
}
테스트는 코드의 품질을 보장하는 핵심이다.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void 사용자_생성_성공() {
// Given
User user = new User("김철수", "kim@example.com", 25);
User savedUser = new User("김철수", "kim@example.com", 25);
savedUser.setId(1L);
when(userRepository.findByEmail("kim@example.com")).thenReturn(Optional.empty());
when(userRepository.save(user)).thenReturn(savedUser);
// When
User result = userService.createUser(user);
// Then
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("김철수");
verify(userRepository).save(user);
}
@Test
void 중복_이메일로_사용자_생성_실패() {
// Given
User user = new User("김철수", "kim@example.com", 25);
User existingUser = new User("이영희", "kim@example.com", 30);
when(userRepository.findByEmail("kim@example.com")).thenReturn(Optional.of(existingUser));
// When & Then
assertThatThrownBy(() -> userService.createUser(user))
.isInstanceOf(RuntimeException.class)
.hasMessage("이미 존재하는 이메일입니다");
}
}
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@AfterEach
void tearDown() {
userRepository.deleteAll();
}
@Test
void 사용자_생성_및_조회_통합_테스트() {
// Given
User user = new User("김철수", "kim@example.com", 25);
// When - 사용자 생성
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", user, User.class);
// Then
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody().getName()).isEqualTo("김철수");
// When - 사용자 조회
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + createResponse.getBody().getId(), User.class);
// Then
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("김철수");
}
}
환경별로 다른 설정을 사용하자.
# application-dev.properties (개발 환경)
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
# application-prod.properties (운영 환경)
spring.datasource.url=jdbc:mysql://localhost:3306/proddb
spring.datasource.username=produser
spring.datasource.password=prodpass
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
# Dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private UserRepository userRepository;
@Override
public Health health() {
try {
long count = userRepository.count();
return Health.up()
.withDetail("database", "Available")
.withDetail("userCount", count)
.build();
} catch (Exception e) {
return Health.down()
.withDetail("database", "Unavailable")
.withDetail("error", e.getMessage())
.build();
}
}
}
자바와 스프링 부트는 현대 웹 개발의 핵심이다. 기본기를 탄탄히 하고, 실전 프로젝트를 통해 경험을 쌓아야 한다.
특히 REST API 설계, 데이터베이스 연동, 테스트 작성은 필수다. 이 모든 것을 마스터하면 어떤 웹 애플리케이션이든 만들 수 있다.