OpenAI 텍스트-음성
OpenAI 텍스트-음성 (TTS)
OpenAI Audio API는 TTS(text-to-speech) 모델 기반의 speech 엔드포인트를 제공해서, 블로그 글을 내레이션하거나, 여러 언어로 음성 오디오를 만들거나, 스트리밍으로 실시간 오디오 출력을 제공할 수 있게 해줘요. 이 글에서는 의존성 추가와 자동 설정, 음성 옵션, 실시간 스트리밍, 그리고 HTTP 클라이언트 커스터마이징과 마이그레이션 가이드까지 폭넓게 다뤄볼게요.
출처: 문서
본문
Introduction
Audio API는 OpenAI의 TTS(text-to-speech) 모델을 기반으로 하는 speech 엔드포인트를 제공해서, 사용자가 다음을 할 수 있게 해줘요:
-
작성한 블로그 글을 내레이션하기.
-
여러 언어로 음성 오디오 만들기.
-
스트리밍을 사용해 실시간 오디오 출력 제공하기.
| __ | 2.0.0-M5 버전부터 Spring AI는 모든 OpenAI 모델을 위해 내부적으로 공식 openai-java SDK를 사용해요. 전환은 매끄러울 것으로 예상되며, OpenAI API 프로퍼티와 빌더의 기존 사용자에게는 호환성이 깨지는 변경이 없어요. 문제를 발견하면 Spring AI GitHub Issues로 알려주세요. |
|---|
Prerequisites
-
OpenAI 계정을 만들고 API 키를 받으세요. OpenAI signup page에서 가입하고, API Keys page에서 API 키를 생성할 수 있어요.
-
프로젝트의 빌드 파일에
spring-ai-openai의존성을 추가하세요. 자세한 내용은 Dependency Management 섹션을 참고해주세요.
Auto-configuration
| __ | Spring AI 자동 설정과 스타터 모듈 아티팩트 이름에 큰 변경이 있었어요. 자세한 내용은 upgrade notes를 참고해주세요. |
|---|
Spring AI는 OpenAI Text-to-Speech 클라이언트에 대한 Spring Boot 자동 설정을 제공해요. 활성화하려면 프로젝트의 Maven pom.xml 파일에 다음 의존성을 추가하세요:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
또는 Gradle build.gradle 빌드 파일에 추가할 수도 있어요:
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
| __ | 빌드 파일에 Spring AI BOM을 추가하려면 Dependency Management 섹션을 참고해주세요. |
|---|
Speech Properties
Connection Properties
프리픽스 spring.ai.openai는 OpenAI에 연결할 수 있게 해주는 프로퍼티 프리픽스예요.
| Property | Description | Default |
|---|---|---|
| spring.ai.openai.base-url | 연결할 URL | api.openai.com |
| spring.ai.openai.api-key | API 키 | - |
| spring.ai.openai.organization-id | 선택적으로 API 요청에 사용할 조직을 지정할 수 있어요. | - |
| spring.ai.openai.project-id | 선택적으로 API 요청에 사용할 프로젝트를 지정할 수 있어요. | - |
| spring.ai.openai.timeout | OpenAI 클라이언트의 요청 타임아웃. | 60 sec. |
| spring.ai.openai.max-retries | OpenAI 클라이언트의 최대 재시도 횟수. | 3 |
| spring.ai.openai.proxy | OpenAI 클라이언트의 프록시 설정. | - |
| spring.ai.openai.custom-headers | OpenAI 클라이언트 요청에 추가할 커스텀 HTTP 헤더. | empty |
| spring.ai.openai.connection-pool-metrics-enabled | 기저 OkHttp 클라이언트의 연결 풀 메트릭을 활성화할지 여부. | false |
| __ | 여러 조직에 속한 사용자(또는 레거시 사용자 API 키로 프로젝트에 접근하는 사용자)의 경우, 선택적으로 API 요청에 사용할 조직과 프로젝트를 지정할 수 있어요. 이 API 요청의 사용량은 지정된 조직과 프로젝트의 사용량으로 집계돼요. |
|---|
Configuration Properties
| __ | 오디오 음성 자동 설정의 활성화/비활성화는 이제 프리픽스 spring.ai.model.audio.speech가 붙은 최상위 프로퍼티로 설정해요. 활성화하려면 spring.ai.model.audio.speech=openai (기본값으로 활성화됨). 비활성화하려면 spring.ai.model.audio.speech=none (또는 openai와 일치하지 않는 어떤 값). 이 변경은 여러 모델의 설정을 허용하기 위한 것이에요. |
|---|
프리픽스 spring.ai.openai.audio.speech는 OpenAI Text-to-Speech 클라이언트를 구성할 수 있게 해주는 프로퍼티 프리픽스예요.
| Property | Description | Default |
|---|---|---|
| spring.ai.model.audio.speech | Audio Speech Model 활성화 | openai |
| spring.ai.openai.audio.speech.base-url | 연결할 URL | api.openai.com |
| spring.ai.openai.audio.speech.api-key | API 키 | - |
| spring.ai.openai.audio.speech.organization-id | 선택적으로 API 요청에 사용할 조직을 지정할 수 있어요. | - |
| spring.ai.openai.audio.speech.project-id | 선택적으로 API 요청에 사용할 프로젝트를 지정할 수 있어요. | - |
| spring.ai.openai.audio.speech.model | 오디오 생성에 사용할 모델의 ID. 사용 가능한 모델: gpt-4o-mini-tts (기본값, 속도와 비용에 최적화), gpt-4o-tts (더 높은 품질), tts-1 (레거시, 속도에 최적화), 또는 tts-1-hd (레거시, 품질에 최적화). |
gpt-4o-mini-tts |
| spring.ai.openai.audio.speech.voice | 합성에 사용할 음성. OpenAI TTS API의 경우, 선택한 모델에 사용 가능한 음성 중 하나: alloy, echo, fable, onyx, nova, shimmer. | alloy |
| spring.ai.openai.audio.speech.response-format | 오디오 출력의 형식. 지원되는 형식은 mp3, opus, aac, flac, wav, pcm. | mp3 |
| spring.ai.openai.audio.speech.speed | 음성 합성의 속도. 허용 범위는 0.25(가장 느림)에서 4.0(가장 빠름). | 1.0 |
| spring.ai.openai.audio.speech.instructions | 음성 전달을 위한 제어 지침 (예: 톤, 속도). gpt-4o-mini-tts 같은 모델에서만 지원되며, tts-1과 tts-1-hd에서는 무시돼요. |
- |
| __ | 공통 spring.ai.openai.base-url, spring.ai.openai.api-key, spring.ai.openai.organization-id, spring.ai.openai.project-id 프로퍼티를 재정의할 수 있어요. spring.ai.openai.audio.speech.base-url, spring.ai.openai.audio.speech.api-key, spring.ai.openai.audio.speech.organization-id, spring.ai.openai.audio.speech.project-id 프로퍼티가 설정되면 공통 프로퍼티보다 우선해요. 서로 다른 모델과 서로 다른 모델 엔드포인트에 서로 다른 OpenAI 계정을 사용하려 할 때 유용해요. |
|---|
| __ | spring.ai.openai.audio.speech 프리픽스가 붙은 모든 프로퍼티는 런타임에 재정의할 수 있어요. |
|---|
Runtime Options
OpenAiAudioSpeechOptions 클래스는 텍스트-음성 요청을 할 때 사용할 옵션을 제공해요. 시작 시 spring.ai.openai.audio.speech로 지정한 옵션이 사용되지만 런타임에 이를 재정의할 수 있어요.
OpenAiAudioSpeechOptions 클래스는 TextToSpeechOptions 인터페이스를 구현하며, 이식 가능한 옵션과 OpenAI 특정 설정 옵션을 모두 제공해요.
예:
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.model("gpt-4o-mini-tts")
.voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.speed(1.0)
.instructions("Friendly; warm tone; natural pauses")
.build();
TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
Manual Configuration
프로젝트의 Maven pom.xml 파일에 spring-ai-openai 의존성을 추가하세요:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
또는 Gradle build.gradle 빌드 파일에 추가할 수도 있어요:
dependencies {
implementation 'org.springframework.ai:spring-ai-openai'
}
| __ | 빌드 파일에 Spring AI BOM을 추가하려면 Dependency Management 섹션을 참고해주세요. |
|---|
다음으로 OpenAiAudioSpeechModel을 만들어볼게요:
var openAiAudioApi = new OpenAiAudioApi()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build();
var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);
var speechOptions = OpenAiAudioSpeechOptions.builder()
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.speed(1.0)
.model(OpenAiAudioApi.TtsModel.GPT_4_O_MINI_TTS.value)
.build();
var speechPrompt = new TextToSpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
TextToSpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
// Accessing metadata (rate limit info)
OpenAiAudioSpeechResponseMetadata metadata = (OpenAiAudioSpeechResponseMetadata) response.getMetadata();
byte[] responseAsBytes = response.getResult().getOutput();
Streaming Real-time Audio
Speech API는 chunk transfer encoding을 사용한 실시간 오디오 스트리밍을 지원해요. 즉 전체 파일이 생성되고 접근 가능해지기 전에도 오디오를 재생할 수 있어요.
OpenAiAudioSpeechModel은 StreamingTextToSpeechModel 인터페이스를 구현해서 표준 및 스트리밍 기능을 모두 제공해요.
var openAiAudioApi = new OpenAiAudioApi()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build();
var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.voice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
.speed(1.0)
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.model(OpenAiAudioApi.TtsModel.GPT_4_O_MINI_TTS.value)
.build();
TextToSpeechPrompt speechPrompt = new TextToSpeechPrompt("Today is a wonderful day to build something people love!", speechOptions);
Flux<TextToSpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);
// You can also stream raw audio bytes directly
Flux<byte[]> audioByteStream = openAiAudioSpeechModel.stream("Hello, world!");
스트림의 각 요소는 최대 8KB의 오디오를 담아요. 8KB는 OpenAiAudioSpeechModel이 기저 HTTP 응답에서 바이트를 끌어올 때 사용하는 내부 읽기 버퍼 크기예요. 각 읽기 시점에 OpenAI에서 도착한 데이터 양에 따라 청크는 그보다 작을 수 있어요.
| __ | 이 스트림을 블로킹 java.io.InputStream만 받는 API에 넘겨야 한다면, 중요한 실무 관례와 테스트된 참조 구현은 Consuming the Stream as a Blocking InputStream을 참고해주세요. |
|---|
각 열린 스트림은 응답 기간 내내 스레드 하나를 점유해요. 그래서 OpenAiAudioSpeechModel은 Reactor의 공유 Schedulers.boundedElastic() 대신 전용 bounded-elastic 스케줄러에서 스트리밍을 실행해서, 과중한 음성 스트리밍 부하가 애플리케이션의 다른 곳에서 이루어지는 무관한 블로킹 작업을 굶기지 않도록 해요. OpenAiAudioSpeechModel.builder().streamScheduler(customScheduler)로 재정의해서 컴포넌트 간에 스케줄러를 공유하거나, 예상되는 동시 스트림 부하에 맞게 풀 크기를 조정할 수 있어요.
Customizing the HTTP Client
Spring AI는 내부적으로 공식 openai-java SDK를 사용하며, SpringAiOpenAiHttpClient.Builder가 만든 커스텀 OkHttp 클라이언트로 HTTP 전송을 구성해요. 기저 OkHttpClient가 생성되기 전에 하나 이상의 OpenAiHttpClientBuilderCustomizer 빈을 노출해서 그 빌더를 가로챌 수 있어요. 각 커스터마이저는 모든 OpenAI 모델(chat, embedding, image, audio, moderation)이 사용하는 동일한 빌더를 받으므로, 커스터마이징이 균일하게 적용돼요.
@FunctionalInterface
public interface OpenAiHttpClientBuilderCustomizer {
void customize(SpringAiOpenAiHttpClient.Builder builder);
}
전형적인 사용 사례:
-
OkHttp
Interceptor인스턴스 등록 (인증, 전파 헤더, 커스텀 로깅); -
디스패처
ExecutorService교체 (예: 비동기 I/O를 가상 스레드로 라우팅); -
빌더가 노출하는 프록시, SSL, 호스트 이름 검증, 연결 풀 크기 구성.
커스터마이저가 여러 개 있으면 @Order / Ordered 순서로, Spring AI 기본값 이후에 적용되므로 사용자 코드가 우선해요.
OpenAi*Model.Builder를 통해 모델을 수동으로 연결할 때도 동일한 훅을 사용할 수 있어요:
var chatModel = OpenAiChatModel.builder()
.options(OpenAiChatOptions.builder().model("gpt-4o").build())
.httpClientBuilderCustomizer(myCustomizer)
.build();
Migration Guide
더 이상 사용하지 않는(deprecated) SpeechModel과 SpeechPrompt 클래스에서 업그레이드한다면, 이 가이드가 새로운 공유 인터페이스로 마이그레이션하는 자세한 지침을 제공해요.
Breaking Changes Summary
이 마이그레이션에는 다음의 호환성이 깨지는 변경이 포함돼요:
-
Removed Classes : 여섯 개의 deprecated 클래스가
org.springframework.ai.openai.audio.speech패키지에서 제거됐어요 -
Package Changes : 핵심 TTS 클래스가
org.springframework.ai.audio.tts패키지로 이동했어요 -
Type Changes :
speed매개변수가 모든 OpenAI TTS 컴포넌트에서Float에서Double로 변경됐어요 -
Interface Hierarchy :
TextToSpeechModel이 이제StreamingTextToSpeechModel을 확장해요
Class Mapping Reference
| Deprecated (Removed) | New Interface |
|---|---|
SpeechModel |
TextToSpeechModel |
StreamingSpeechModel |
StreamingTextToSpeechModel |
SpeechPrompt |
TextToSpeechPrompt |
SpeechResponse |
TextToSpeechResponse |
SpeechMessage |
TextToSpeechMessage |
Speech (in org.springframework.ai.openai.audio.speech) |
Speech (in org.springframework.ai.audio.tts) |
Step-by-Step Migration Instructions
Step 1: Update Imports
이전 org.springframework.ai.openai.audio.speech 패키지의 모든 import를 새로운 공유 인터페이스로 바꿔주세요:
Find: import org.springframework.ai.openai.audio.speech.SpeechModel;
Replace: import org.springframework.ai.audio.tts.TextToSpeechModel;
Find: import org.springframework.ai.openai.audio.speech.StreamingSpeechModel;
Replace: import org.springframework.ai.audio.tts.StreamingTextToSpeechModel;
Find: import org.springframework.ai.openai.audio.speech.SpeechPrompt;
Replace: import org.springframework.ai.audio.tts.TextToSpeechPrompt;
Find: import org.springframework.ai.openai.audio.speech.SpeechResponse;
Replace: import org.springframework.ai.audio.tts.TextToSpeechResponse;
Find: import org.springframework.ai.openai.audio.speech.SpeechMessage;
Replace: import org.springframework.ai.audio.tts.TextToSpeechMessage;
Find: import org.springframework.ai.openai.audio.speech.Speech;
Replace: import org.springframework.ai.audio.tts.Speech;
Step 2: Update Type References
코드의 모든 타입 참조를 바꿔주세요:
Find: SpeechModel
Replace: TextToSpeechModel
Find: StreamingSpeechModel
Replace: StreamingTextToSpeechModel
Find: SpeechPrompt
Replace: TextToSpeechPrompt
Find: SpeechResponse
Replace: TextToSpeechResponse
Find: SpeechMessage
Replace: TextToSpeechMessage
Step 3: Update Speed Parameter (Float → Double)
speed 매개변수가 Float에서 Double로 변경됐어요. 모든 발생 지점을 업데이트하세요:
Find: .speed(1.0f)
Replace: .speed(1.0)
Find: .speed(0.5f)
Replace: .speed(0.5)
Find: Float speed
Replace: Double speed
Float 값이 있는 직렬화된 데이터나 설정 파일이 있다면 그것도 업데이트해야 해요:
// Before
{
"speed": 1.0
}
// After (no code change needed for JSON, but be aware of type change in Java)
{
"speed": 1.0
}
Step 4: Update Bean Declarations
Spring Boot 자동 설정이나 수동 빈 정의가 있다면:
// Before
@Bean
public SpeechModel speechModel(OpenAiAudioApi audioApi) {
return new OpenAiAudioSpeechModel(audioApi);
}
// After
@Bean
public TextToSpeechModel textToSpeechModel(OpenAiAudioApi audioApi) {
return new OpenAiAudioSpeechModel(audioApi);
}
Code Migration Examples
Example 1: Basic Text-to-Speech Conversion
Before (deprecated):
import org.springframework.ai.openai.audio.speech.*;
@Service
public class OldNarrationService {
private final SpeechModel speechModel;
public OldNarrationService(SpeechModel speechModel) {
this.speechModel = speechModel;
}
public byte[] createNarration(String text) {
SpeechPrompt prompt = new SpeechPrompt(text);
SpeechResponse response = speechModel.call(prompt);
return response.getResult().getOutput();
}
}
After (using shared interfaces):
import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
@Service
public class NarrationService {
private final TextToSpeechModel textToSpeechModel;
public NarrationService(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
public byte[] createNarration(String text) {
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text);
TextToSpeechResponse response = textToSpeechModel.call(prompt);
return response.getResult().getOutput();
}
}
Example 2: Text-to-Speech with Custom Options
Before (deprecated):
import org.springframework.ai.openai.audio.speech.*;
import org.springframework.ai.openai.api.OpenAiAudioApi;
SpeechModel model = new OpenAiAudioSpeechModel(audioApi);
OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder()
.model("tts-1")
.voice(OpenAiAudioApi.SpeechRequest.Voice.NOVA)
.speed(1.0f) // Float value
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.build();
SpeechPrompt prompt = new SpeechPrompt("Hello, world!", options);
SpeechResponse response = model.call(prompt);
byte[] audio = response.getResult().getOutput();
After (using shared interfaces):
import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;
TextToSpeechModel model = new OpenAiAudioSpeechModel(audioApi);
OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder()
.model("tts-1")
.voice(OpenAiAudioApi.SpeechRequest.Voice.NOVA)
.speed(1.0) // Double value
.responseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.build();
TextToSpeechPrompt prompt = new TextToSpeechPrompt("Hello, world!", options);
TextToSpeechResponse response = model.call(prompt);
byte[] audio = response.getResult().getOutput();
Example 3: Streaming Text-to-Speech
Before (deprecated):
import org.springframework.ai.openai.audio.speech.*;
import reactor.core.publisher.Flux;
StreamingSpeechModel model = new OpenAiAudioSpeechModel(audioApi);
SpeechPrompt prompt = new SpeechPrompt("Stream this text");
Flux<SpeechResponse> stream = model.stream(prompt);
stream.subscribe(response -> {
byte[] audioChunk = response.getResult().getOutput();
// Process audio chunk
});
After (using shared interfaces):
import org.springframework.ai.audio.tts.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import reactor.core.publisher.Flux;
TextToSpeechModel model = new OpenAiAudioSpeechModel(audioApi);
TextToSpeechPrompt prompt = new TextToSpeechPrompt("Stream this text");
Flux<TextToSpeechResponse> stream = model.stream(prompt);
stream.subscribe(response -> {
byte[] audioChunk = response.getResult().getOutput();
// Process audio chunk
});
Example 4: Dependency Injection with Spring Boot
Before (deprecated):
@RestController
public class OldSpeechController {
private final SpeechModel speechModel;
@Autowired
public OldSpeechController(SpeechModel speechModel) {
this.speechModel = speechModel;
}
@PostMapping("/narrate")
public ResponseEntity<byte[]> narrate(@RequestBody String text) {
SpeechPrompt prompt = new SpeechPrompt(text);
SpeechResponse response = speechModel.call(prompt);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("audio/mpeg"))
.body(response.getResult().getOutput());
}
}
After (using shared interfaces):
@RestController
public class SpeechController {
private final TextToSpeechModel textToSpeechModel;
@Autowired
public SpeechController(TextToSpeechModel textToSpeechModel) {
this.textToSpeechModel = textToSpeechModel;
}
@PostMapping("/narrate")
public ResponseEntity<byte[]> narrate(@RequestBody String text) {
TextToSpeechPrompt prompt = new TextToSpeechPrompt(text);
TextToSpeechResponse response = textToSpeechModel.call(prompt);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("audio/mpeg"))
.body(response.getResult().getOutput());
}
}
Spring Boot Configuration Changes
Spring Boot 자동 설정 프로퍼티는 동일하게 유지돼요. application.properties나 application.yml 파일을 변경할 필요가 없어요.
하지만 명시적인 빈 참조나 qualifier가 있다면 업데이트하세요:
// Before
@Qualifier("speechModel")
// After
@Qualifier("textToSpeechModel")
Benefits of the Migration
-
Portability : 코드를 한 번 작성하고 OpenAI, ElevenLabs 또는 다른 TTS 제공자 간에 쉽게 전환
-
Consistency : ChatModel 및 다른 Spring AI 추상화와 동일한 패턴
-
Type Safety : 적절한 인터페이스 구현으로 개선된 타입 계층 구조
-
Future-Proof : 새로운 TTS 제공자가 기존 코드와 자동으로 동작
-
Standardization : 모든 TTS 제공자에서 speed 매개변수의 일관된
Double타입
Common Migration Issues and Solutions
Issue 1: Compilation Error - Cannot Find Symbol SpeechModel
Error:
error: cannot find symbol SpeechModel
Solution: Step 1에서 설명한 대로 import를 업데이트하고, SpeechModel을 TextToSpeechModel로 바꾸세요.
Issue 2: Type Mismatch - Float Cannot Be Converted to Double
Error:
error: incompatible types: float cannot be converted to Double
Solution: 부동소수점 리터럴의 f 접미사를 제거하세요 (예: 1.0f를 1.0으로 변경).
Issue 3: Bean Creation Error at Runtime
Error:
NoSuchBeanDefinitionException: No qualifying bean of type 'SpeechModel'
Solution: 의존성 주입을 SpeechModel 대신 TextToSpeechModel을 사용하도록 업데이트하세요.
Example Code
- OpenAiAudioSpeechModelIT.java 테스트는 라이브러리 사용법에 대한 일반적인 예제를 제공해요.